[tryton-debian-vcs] suds branch py3-drop_in_suds_jurko-WIP created. debian/0.4.1-14-7-g2302134

Mathias Behrle tryton-debian-vcs at alioth.debian.org
Wed Jul 16 11:56:53 UTC 2014


The following commit has been merged in the py3-drop_in_suds_jurko-WIP branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/suds.git;a=commitdiff;h=debian/0.4.1-14-7-g2302134
commit 23021348e9a7a87946c44772a4c4a4846c1a0ae5
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Wed Jul 16 13:47:22 2014 +0200

    Committing WIP of drop-in package suds-jurko into suds.
    
    - Uses a self generated tarball suds_0.7~dev0.orig.tar.bz2 from
      30.6.2014
    - Waits for renaming of the suds-jurko project on pypi to suds.
    - Waits for release 0.7 according to
      https://lists.debian.org/debian-python/2014/07/msg00020.html

diff --git a/debian/changelog b/debian/changelog
index 6c6396e..21c332c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+suds (0.7~dev0-1) unstable; urgency=medium
+
+  * Adding upstream version 0.7.dev0.
+
+ -- Mathias Behrle <mathiasb at m9s.biz>  Mon, 30 Jun 2014 19:15:50 +0200
+
 suds (0.6-1) unstable; urgency=medium
 
   * Adding upstream version 0.6.
diff --git a/debian/control b/debian/control
index 86ef56d..1c13dab 100644
--- a/debian/control
+++ b/debian/control
@@ -4,8 +4,8 @@ Priority: optional
 Maintainer: Debian Tryton Maintainers <maintainers at debian.tryton.org>
 Uploaders: Mathias Behrle <mathiasb at m9s.biz>
 Build-Depends:
- debhelper (>= 9), python (>= 2.6.6-3~), python-setuptools, dh-python,
- python-pytest
+ debhelper (>= 9), python (>= 2.6.6-3~), python3 (>= 3.3.0-2~), dh-python,
+ python-setuptools, python3-setuptools, python-pytest, python3-pytest
 Standards-Version: 3.9.5
 Homepage: http://www.fedorahosted.org/suds/
 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=tryton/suds.git
@@ -15,9 +15,23 @@ X-Python-Version: >= 2.6
 Package: python-suds
 Architecture: all
 Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources
-Description: Lightweight SOAP client for Python
+Description: Lightweight SOAP client for Python (implemented in Python 2)
  The suds project is a Python soap web services client lib. Suds leverages
  Python meta programming to provide an intuitive API for consuming web services.
  Objectification of types defined in the WSDL is provided without class
  generation. Programmers rarely need to read the WSDL since services and WSDL
  based objects can be easily inspected. Supports pluggable soap bindings.
+ .
+ This package is targeting Python version 2.
+
+Package: python3-suds
+Architecture: all
+Depends: ${misc:Depends}, ${python3:Depends}, python3-pkg-resources
+Description: Lightweight SOAP client for Python (implemented in Python 3)
+ The suds project is a Python soap web services client lib. Suds leverages
+ Python meta programming to provide an intuitive API for consuming web services.
+ Objectification of types defined in the WSDL is provided without class
+ generation. Programmers rarely need to read the WSDL since services and WSDL
+ based objects can be easily inspected. Supports pluggable soap bindings.
+ .
+ This package is targeting Python version 3.
diff --git a/debian/python3-suds.docs b/debian/python3-suds.docs
new file mode 100644
index 0000000..d5f6c65
--- /dev/null
+++ b/debian/python3-suds.docs
@@ -0,0 +1,3 @@
+README.rst
+TODO.txt
+notes/
diff --git a/debian/rules b/debian/rules
index f18b27f..f0899e5 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,6 +1,12 @@
 #!/usr/bin/make -f
 
+PACKAGE_NAME := $(shell python setup.py --name)
+#export PYBUILD_NAME=$(PACKAGE_NAME)
+export PYBUILD_NAME=suds
+
 %:
 	dh ${@} --with python2 --buildsystem=pybuild
 
-
+override_dh_auto_clean:
+	dh_auto_clean
+	rm -rf tools/__dummy__/
commit ffddb2358fc4a826bfcd83a0d7d3aac679231101
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:54:40 2014 +0200

    Adding upstream version 0.7.dev0.

diff --git a/HACKING.rst b/HACKING.rst
index 1940d16..cb6d883 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -10,159 +10,23 @@ Project development should be tracked in the ``TODO.txt`` file.
   consistently.
 * Done tasks should be marked as such and not deleted.
 
-Testing:
-
-* ``pytest`` testing framework needed to run unit tests.
-* To run the tests using Python 3 first process them and the rest of the library
-  sources using the Python ``py2to3`` conversion tool.
-* For more detailed information see the `DEVELOPMENT & TESTING ENVIRONMENT`_
-  section below.
-
-Reproducing problematic use cases:
-
-* Failing web service processing examples can be easily packaged as reproducible
-  test cases using the suds library 'message & reply injection' technique.
-* Some things you can achieve using this technique (for examples, see existing
-  project unit tests):
-
-  * Create a client object based on a fixed WSDL string.
-  * Have a client object send a fixed request string without having it construct
-    one based on the loaded WSDL schema and received arguments.
-  * Have a client object process a fixed reply string without having it send a
-    request to an actual external web service.
-
-Base sources should remain Python 2.x compatible. Since the original project
-states aiming for Python 2.4 compatibility we should do so as well.
-
-Python features that need to be avoided for compatibility with older Python
-versions:
-
-* Features introduced in Python 2.5.
-
-  * ``any`` & ``all`` functions.
-  * ``with`` statement.
-  * ``try``/``except``/``finally`` blocks.
-
-    * Prior to this Python release, code like the following::
-
-        try:
-            A
-        except XXX:
-            B
-        finally:
-            C
-
-      was considered illegal and needed to be written using nested ``try``
-      blocks as in::
-
-        try:
-            try:
-                A
-            except XXX:
-                B
-        finally:
-            C
-
-  * ``yield`` expression inside a ``try`` block with a ``finally`` clause.
-
-    * Prior to this Python release, code like the following::
-
-        try:
-            yield x
-        finally:
-            do_something()
-
-      is considered illegal, but can be replaced with legal code similar to the
-      following::
-
-        try:
-            yield x
-        except:
-            do_something()
-            raise
-        do_something()
-
-* Features introduced in Python 2.6.
-
-  * ``bytes`` type.
-  * Byte literals, e.g. ``b"quack"``.
-  * String ``format()`` method.
-
-* Features introduced in Python 2.7.
-
-  * Dictionary & set comprehensions.
-  * Set literals.
-
-Handling Python 3 related patches applicable to the original suds development
-project.
-
-* Originally synchronized with the `Mercurial patch queue
-  <http://bitbucket.org/bernh/suds-python-3-patches>`_ maintained by Bernhard
-  Leiner.
-* Used to be kept synchronized with the originating queue but no longer. It
-  became more and more difficult to keep them synchronized as our work
-  progressed and both the original suds project and this patch queue seem dead
-  otherwise.
-* Used to be committed first to the 'Python 3 support' branch and then merged
-  back to the trunk from there.
-
-External documentation:
-
-* SOAP
-
-  * http://www.w3.org/TR/soap
-
-  * Version 1.1.
-
-    * http://www.w3.org/TR/2000/NOTE-SOAP-20000508
-
-  * Version 1.2.
-
-    * Part0: Primer
-
-      * http://www.w3.org/TR/2007/REC-soap12-part0-20070427
-      * Errata: http://www.w3.org/2007/04/REC-soap12-part0-20070427-errata.html
-
-    * Part1: Messaging Framework
-
-      * http://www.w3.org/TR/2007/REC-soap12-part1-20070427
-      * Errata: http://www.w3.org/2007/04/REC-soap12-part1-20070427-errata.html
-
-    * Part2: Adjuncts
-
-      * http://www.w3.org/TR/2007/REC-soap12-part2-20070427
-      * Errata: http://www.w3.org/2007/04/REC-soap12-part2-20070427-errata.html
-
-    * Specification Assertions and Test Collection
-
-      * http://www.w3.org/TR/2007/REC-soap12-testcollection-20070427
-      * Errata:
-        http://www.w3.org/2007/04/REC-soap12-testcollection-20070427-errata.html
-
-* WS-I Basic Profile 1.1
-
-  * http://www.ws-i.org/Profiles/BasicProfile-1.1.html
-
-* WSDL 1.1
-
-  * http://www.w3.org/TR/wsdl
-
-* XML Schema
-
-  * Part 0: Primer Second Edition - http://www.w3.org/TR/xmlschema-0
-
-    * Non-normative document intended to provide an easily readable description
-      of the XML Schema facilities, and is oriented towards quickly
-      understanding how to create schemas using the XML Schema language.
-
-  * Part 1: Structures - http://www.w3.org/TR/xmlschema-1
-  * Part 2: Datatypes - http://www.w3.org/TR/xmlschema-2
+Separate sections below:
+
+* `TOP-LEVEL PROJECT FILES & FOLDERS`_
+* `PYTHON COMPATIBILITY`_
+* `RELEASE PROCEDURE`_
+* `DEVELOPMENT & TESTING ENVIRONMENT`_
+* `PYTHON 2/3 SOURCE CODE COMPATIBILITY`_
+* `EXTERNAL DOCUMENTATION`_
+* `STANDARDS CONFORMANCE`_
+* `PROJECT IMPLEMENTATION NOTES`_
+* `REPRODUCING PROBLEMATIC USE CASES`_
 
 For additional design, research & development project notes see the project's
 ``notes/`` folder.
 
 
-TOP-LEVEL FILES & FOLDERS
+TOP-LEVEL PROJECT FILES & FOLDERS
 =================================================
 
 | .hg/
@@ -189,6 +53,11 @@ TOP-LEVEL FILES & FOLDERS
 
 * Project test code.
 
+| tools/
+
+* Project development & setup utility scripts. Related internal Python modules
+  are located under its ``suds_devel/`` package folder.
+
 | MANIFEST.in
 
 * Build system configuration file listing the files to be included in the
@@ -202,11 +71,6 @@ TOP-LEVEL FILES & FOLDERS
 
 * Internal project documentation.
 
-| run_all_tests.cmd
-
-* Basic development script for running the full project test suite using
-  multiple Python interpreter versions on a Windows development machine.
-
 | setup.cfg
 
 * Basic project Python configuration.
@@ -219,22 +83,152 @@ TOP-LEVEL FILES & FOLDERS
 
   ``setup.py --help``
     show detailed usage information
+  ``setup.py --help-commands``
+    show detailed ``setup.py`` command list
   ``setup.py build``
     build the project
-  ``setup.py develop``
-    prepare the development environment (add the project folder to the
-    Python module search path)
   ``setup.py install``
     build & install the project
   ``setup.py register``
     register a project release at PyPI
   ``setup.py sdist``
     prepare a source distribution
-  ``setup.py test``
-    run the project's test suite (requires ``pytest``)
   ``setup.py upload``
     upload prepared packages to PyPI
 
+* Usage examples requiring ``setuptools``:
+
+  ``setup.py develop``
+    prepare the development environment (add the project folder to the Python
+    module search path) the same as if installed using ``easy_install -e`` or
+    ``pip install -e``
+  ``setup.py test``
+    run the project test suite (requires ``pytest``)
+
+
+PYTHON COMPATIBILITY
+=================================================
+
+Base sources should remain Python 2.x compatible. Since the original project
+states aiming for Python 2.4 compatibility we do so as well.
+
+The Python 3.0 minor release is not supported. See `Python 3.0 support`_
+subsection below for more detailed information.
+
+Test & setup code needs to be implemented using Python 2 & 3 compatible source
+code. Setup & setup related scripts need to be implemented so they do not rely
+on other pre-installed libraries.
+
+These backward compatibility requirements do not affect internal development
+operations such as ``setup.py`` support for uploading a new project distribution
+package to PyPI. Such operations need to be supported on the latest Python 2 & 3
+releases only and no additional backward compatibility is either tested or
+guaranteed for them.
+
+The following is a list of backward incompatible Python features not used in
+this project to maintain backward compatibility:
+
+Features missing prior to Python 2.5
+------------------------------------
+
+* ``any`` & ``all`` functions.
+* ``with`` statement.
+* BaseException class introduced and KeyboardInterrupt & SystemExit exception
+  classes stopped being Exception subclasses.
+
+  * This means that code wanting to support Python versions prior to this
+    release needs to re-raise KeyboardInterrupt & SystemExit exceptions before
+    handling the generic 'Exception' case, unless it really wants to gobble up
+    those special infrastructural exceptions as well.
+
+* ``try``/``except``/``finally`` blocks.
+
+  * Prior to this Python release, code like the following::
+
+      try:
+          A
+      except XXX:
+          B
+      finally:
+          C
+
+    was considered illegal and needed to be written using nested ``try`` blocks
+    as in::
+
+      try:
+          try:
+              A
+          except XXX:
+              B
+      finally:
+          C
+
+* ``yield`` expression inside a ``try`` block with a ``finally`` clause.
+
+  * Prior to this Python release, code like the following::
+
+      try:
+          yield x
+      finally:
+          do_something()
+
+    is considered illegal, but can be replaced with legal code similar to the
+    following::
+
+      try:
+          yield x
+      except:
+          do_something()
+          raise
+      do_something()
+
+Features missing prior to Python 2.6
+------------------------------------
+
+* ``bytes`` type.
+* Byte literals, e.g. ``b"quack"``.
+* Class decorators.
+* ``fractions`` module.
+* ``numbers`` module.
+* String ``format()`` method.
+
+Features missing prior to Python 2.7
+------------------------------------
+
+* Dictionary & set comprehensions.
+* Set literals.
+
+Features missing in Python 3.0 & 3.1
+------------------------------------
+
+* py2to3 conversion for source files with an explicitly specified UTF-8 BOM.
+
+
+Python 3.0 support
+------------------
+
+Python 3.0 release has been marked as deprecated almost immediately after the
+release 3.1. It is not expected that this Python release is actively used
+anywhere in the wild. That said, if anyone really wants this version supported
+- patches are welcome.
+
+At least the following problems have been found with Python 3.0:
+
+* None of the tools required to properly test our project (setuptools, pip,
+  virtualenv, tox, etc.) will work on it.
+* When you attempt to setuptools project with Python 3.0, it attempts to use the
+  ``sys.stdout.detach()`` method introduced only in Python 3.1. This specific
+  issue could be worked around by using ``sys.stdout.buffer`` directly but the
+  actual fix has not been attempted. If anyone wants to take this route though
+  and work on supporting setuptools on Python 3.0 - be warned that it will most
+  likely have other issues after this one as well.
+* When applying py2to3 to the project sources, Python will use the current
+  user's locale encoding instead of the one specified in the project sources,
+  thus causing the operation to fail on some source files containing different
+  unicode characters unless the user's environement uses some sort of unicode
+  encoding by default, e.g. will fail on some test scripts when run on Windows
+  with eastern European regional settings (uses the CP1250 encoding).
+
 
 RELEASE PROCEDURE
 =================================================
@@ -244,14 +238,42 @@ RELEASE PROCEDURE
 2. Test the project build with the latest available ``setuptools`` project and
    update the ``ez_setup.py`` ``setuptools`` installation script as needed.
 
-  * Chosen ``setuptools`` version needs to support all the Python interpreter
-    versions supported by our project.
+  * Use the latest available & tested ``setuptools`` release.
+  * If a new ``setuptools`` release drops support for an older Python release,
+    update our ``setup.py`` script to use an older ``setuptools`` installation
+    script when run using the no longer supported Python release.
 
-  * ``setuptools`` version 2.0 dropped support for Python 2.4 & 2.5.
+    * For example, ``setuptools`` version 2.0 dropped support for Python 2.4 &
+      2.5 and so ``setup.py`` uses a separate ``ez_setup_1_4_2.py``
+      ``setuptools`` installation script with Python versions older than 2.6.
 
 3. Version identification.
 
-  * Remove the ``(development)`` suffix for official release builds.
+  * Official releases marked with no extra suffix after the basic version
+    number.
+  * Alfa releases marked with the suffix ``.a#``.
+  * Beta releases marked with the suffix ``.b#``.
+  * Release candidate releases marked with the suffix ``.rc#``.
+  * Development releases marked with the suffix ``.dev#``.
+  * Version ordering (as recognized by pip & setuptools)::
+
+      0.5.dev0 < 0.5.dev1 < 0.5.dev5
+        < 0.5.a0.dev0 < 0.5.a0.dev5 < 0.5.a0
+        < 0.5.a3.dev0 < 0.5.a3.dev5 < 0.5.a3
+        < 0.5.b0.dev0 < 0.5.b0.dev5 < 0.5.b0
+        < 0.5.b3.dev0 < 0.5.b3.dev5 < 0.5.b3
+        < 0.5.rc0.dev0 < 0.5.rc0.dev5 < 0.5.rc0
+        < 0.5.rc3.dev0 < 0.5.rc3.dev5 < 0.5.rc3
+        < 0.5
+      < 0.5.1.dev0 < ...
+        ...
+        < 0.5.1
+      < 0.6.dev0 < ...
+        ...
+        < 0.6
+      < 1.0.dev0 < ...
+        ...
+        < 1.0
 
 4. Tag in Hg.
 
@@ -261,19 +283,32 @@ RELEASE PROCEDURE
 
   * Official releases should always be prepared based on tagged revisions with
     no local changes in the used sandbox.
-  * Prepare source distribution packages (both .zip & .tar.bz2 formats),
-    register the new release at PyPI and upload the prepared source packages.
+  * Prepare source distribution packages (both .zip & .tar.bz2 formats) and
+    upload the prepared source packages to PyPI.
 
-    * Run ``setup.py sdist register upload``.
+    * Run ``setup.py sdist upload``.
 
-  * Upload the prepared source package to the project site.
+  * Prepare wheel packages for Python 2 & 3 using the latest Python 2 & 3
+    environments with the ``wheel`` package installed and upload them to PyPI.
+
+    * Run ``setup.py bdist_wheel upload`` using both Python 2 & 3.
+
+  * Upload the prepared source & wheel packages to the project site.
 
     * Use the BitBucket project web interface.
 
 6. Next development version identification.
 
-  * Bump up the forked project version counter.
-  * Add back the ``(development)`` suffix, e.g. as in ``0.5 (development)``.
+  * If this was a development release.
+
+    * Bump up the existing ``.dev#`` suffix, e.g. change ``0.8.dev2`` to
+      ``0.8.dev3``.
+
+  * If this was a non-development release.
+
+    * Bump up the forked project version counter (may add/remove/bump
+      alfa/beta/release-candidate mark suffixes as needed).
+    * Add the ``.dev0`` suffix, e.g. as in ``0.8.dev0``.
 
 7. Notify whomever the new release might concern.
 
@@ -285,46 +320,201 @@ In all command-line examples below pyX, pyXY & pyXYZ represent a Python
 interpreter executable for a specific Python version X, X.Y & X.Y.Z
 respectively.
 
-Testing environment is generally set up as follows:
+Setting up the development & testing environment
+------------------------------------------------
+
+``tools/setup_base_environments.py`` script should be used for setting up the
+basic Python environments so they support testing our project. The script can be
+configured from the main project Python configuration file ``setup.cfg``. It
+implements all the backward compatibility tweaks and performs additional
+required package installation that would otherwise need to be done manually in
+order to be able to test our project in those environments.
+
+These exact requirements and their related version specific tweaks are not
+documented elsewhere so anyone interested in the details should consult the
+script's sources.
+
+The testing environment is generally set up as follows:
+
+1. Install clean target Python environments.
+#. Update the project's ``setup.py`` configuration with information on your
+   installed Python environments.
+#. Run the ``tools/setup_base_environments.py`` script.
+
+Some older Python environments may have slight issues caused by varying support
+levels in different used Python packages, but the basic testing functionality
+has been tested to make sure it works on as wide array of supported platforms as
+possible.
+
+Examples of such issues:
+
+* Colors not getting displayed on a Windows console terminal, with possibly ANSI
+  color code escape sequences getting displayed instead.
+* ``pip`` utility can not be run from the command-line using the ``py -m pip``
+  syntax for some older versions. In such cases use the more portable ``py -c
+  "import pip;pip.main()"`` syntax instead.
+* Some specific older Python versions (e.g. 2.4.3) have no SSL support and so
+  have to reuse installations downloaded by other Python versions.
+
+Running the project tests - ``tools/run_all_tests.py`` script
+-------------------------------------------------------------
+
+``tools/run_all_tests.py`` script is a basic *poor man's tox* development script
+that can be used for running the full project test suite using multiple Python
+interpreter versions on a development machine.
+
+Intended to be replaced by a more portable ``tox`` based or similar automated
+testing solution some time in the future.
+
+Can be configured by tweaking the main project Python configuration file
+``setup.cfg``:
 
-1. Install Python.
-2. Install ``setuptools`` (using ``setup_ez.py`` or from the source
-   distribution).
-3. Install ``pip`` using ``setuptools`` (optional).
-4. Install ``pytest`` using ``pip`` or ``setuptools``.
+* List of target Python environments.
+* Each target Python environment's invocation command.
 
-This should hold for all Python releases except some older ones explicitly
-listed below.
+Requires the target Python environment already be set up, and all the packages
+required for running the project test suite installed. See the `Setting up the
+development & testing environment`_ section for more detailed information.
 
-To run all of the project unit tests with a specific interpreter without
-additional configuration options run the project's ``setup.py`` script with the
-'test' parameter and an appropriate Python interpreter. E.g. run any of the
-following from the top level project folder::
+Automatically installs the project in editable mode in all tested Python
+environments.
+
+Caveats:
+
+* This method does not allow you to provide any extra ``pytest`` options when
+  running the project test suite.
+
+Running the project tests - ``setup.py test`` command
+-----------------------------------------------------
+
+Project tests can also be run for a specific Python environment by running the
+project's ``setup.py`` script in that environment and invoking its ``test``
+command. E.g. run a command like one of the following ones from the top level
+project folder::
 
   py243 setup.py test
   py27 setup.py test
   py3 setup.py test
 
-To have more control over the test suite run it from the top level project
-folder using ``pytest``, e.g.
+Note that the ``setup.py`` script always needs to be called from the top level
+project folder.
+
+For most Python versions, the target Python environment needs not be set up
+prior to running this command. Where possible (e.g. not for Python 2.4.x or
+3.1.x versions), any missing testing requirements will be installed
+automatically, but not directly into the target environment but in the current
+folder instead. This functionality should be considered a band-aid though, and
+setting up the target environment can be better done as described in the
+`Setting up the development & testing environment`_ section.
+
+The ``setup.py test`` command will build the project if needed and run its test
+suite in the target Python environment. The project does not need to be
+preinstalled into the target Python environment for this operation to work, and
+neither will the operation leave it installed.
+
+Unless a more restricted test set is selected using ``pytest`` specific
+command-line options, ``setup.py test`` command runs the complete project test
+suite.
+
+Specific ``pytest`` command-line options may be provided by passing them all as
+a single whitespace separated string tunnelled via the ``setup.py test``
+command's ``--pytest-args``/``-a`` command-line option.
+
+For example, the following command will run only tests containing ``binding`` in
+their name, will stop on first failure and will automatically drop into Python's
+post-mortem debugger on failure::
+
+  setup.py test -a "-k binding -x --pdb"
+
+Caveats:
+
+* This method does not currently allow passing ``pytest`` specific command-line
+  options containing embedded whitespace.
+* When running the ``setup.py test`` command in a Windows Python 2.5 environment
+  without an included ctypes module (e.g. 64-bit CPython 2.5 distribution does
+  not include ctypes) and having it automatically install the colorama package
+  version older than 0.1.11, you will get benign error messages reporting
+  colorama's atexit handlers failing. Running the same command again avoids the
+  issue since the colorama package will then already be installed. Suggested
+  workaround is to use a colorama package version 0.3.2 or newer.
+
+Running the project tests - using ``pytest`` directly
+-----------------------------------------------------
+
+To have greater control over the test suite and be able to specify additional
+``pytest`` options on the command-line, or be able to run the tests on a
+different project installation (e.g. official release installed directly from
+PyPI), do the following:
+
+1. Install the project into the target Python environment.
+
+  * Installing the project can be done by either installing it directly into the
+    target Python environment using one of the following commands (paths used
+    assume the commands are being run from the top level project folder)::
+
+      setup.py install
+      easy_install .
+      pip install .
+
+    Or the project can be installed in editable mode using one of the following
+    commands (so it does not need to be reinstalled after every source code
+    change)::
+
+      setup.py develop
+      easy_install -e .
+      pip install -e .
+
+  * The installation step can be skipped if running Python 2 based project
+    tests, and doing so from the top level project folder.
+
+2. Run tests using ``pytest``.
+
+  * If using Python 2.x:
+
+    * Run ``pytest`` from the project's top level or ``tests`` folder::
+
+        py2 -m pytest
+
+  * If using Python 3.x:
+
+    * Since the project uses py2to3 source conversion, you need to build the
+      project in order to generate the project's Python 3 sources before they
+      can be tested. If the project has been installed in editable mode, then
+      simply run the following from the top level project folder::
 
-* Using a Python 2.x interpreter::
+        setup.py build
 
-    py27 -m pytest
+      and if it has not then rebuild and reinstall it using one of the following
+      commands::
 
-* Using a Python 3.x interpreter::
+        setup.py develop
+        setup.py install
 
-    py33 setup.py build & py33 -m pytest build
+      Note that you might need to manually remove the build folder in order to
+      have its contents regenerated when wanting to run the test suite using a
+      different Python 3.x interpreter version, as those sources are regenerated
+      based solely on the original & processed source file timestamp information
+      and not the Python version used to process them.
 
-This way you can specify additional ``pytest`` options on the command-line.
+    * Run ``pytest`` from the the project's ``tests`` folder::
 
-In both cases, tests run using Python interpreter version 3.x will be run in the
-build folder constructed by the ``setup.py`` script running the ``py2to3`` tool
-on the project's sources. You might need to manually remove the build folder in
-order to have sources in it regenerated when wanting to run the test suite using
-a different Python 3.x interpreter version, as those sources are regenerated
-based solely on the original & processed source file timestamp information and
-not the Python version used to process them.
+        py3 -m pytest
+
+Each specific test module can also be run directly as a script.
+
+Notes on the folder from which to run the tests:
+
+* When running tests from a folder other than the top level project folder, the
+  tested project version needs to first be installed in the used Python
+  environment.
+* Python 2 tests can be run from the top level project folder, in which case
+  they will work even if the project has not been explicitly installed in the
+  used Python environment. And even if another project version has been
+  installed into the used Python environment, that one will be ignored and the
+  one in the current folder used instead.
+* Python 3 tests can not be run from the top level project folder or they would
+  attempt and fail to use Python 2 based project sources found in the current
+  folder.
 
 See the ``pytest`` documentation for a detailed list of available command-line
 options. Some interesting ones:
@@ -332,10 +522,14 @@ options. Some interesting ones:
   -l          show local variable state in tracebacks
   --tb=short  shorter traceback information for each failure
   -x          stop on first failure
+  --pdb       enter Python debugger on failure
+
+Setting up multiple parallel Python interpreter versions on Windows
+-------------------------------------------------------------------
 
 On Windows you might have a problem setting up multiple parallel Python
-interpreter versions in case they match their major and minor version numbers,
-e.g. Python 2.4.3 & 2.4.4. In those cases, standard Windows installer will
+interpreter versions in case their major and minor version numbers match, e.g.
+Python 2.4.3 & 2.4.4. In those cases, standard Windows installer will
 automatically remove the previous installation instead of simply adding a new
 one. In order to achieve such parallel setup we suggest the following steps:
 
@@ -359,87 +553,78 @@ to determine where to install its package data. In that case you can set those
 entries manually, e.g. by using a script similar to the one found at
 `<http://nedbatchelder.com/blog/201007/installing_python_packages_from_windows_installers_into.html>`_.
 
-Notes on setting up specific Python versions
---------------------------------------------
 
-Python 2.4.3
+PYTHON 2/3 SOURCE CODE COMPATIBILITY
+=================================================
 
-* Does not work with HTTPS links so you can not use the Python package index
-  directly, since it, at some point, switched to using HTTPS links only.
+These are notes related to maintaining Python 2/3 source code compatibility in
+parts of this project that require it.
 
-  * You could potentially work around this problem by somehow mapping its https:
-    links to http: ones or download its link page manually, locally modify it to
-    contain http: links and then use that download link page instead of the
-    default downloaded one.
-  * An alternative and tested solution is to install into Python 2.4.4 and then
-    copy all the related site-packages entries from that installation into this
-    one.
+Use the ``six <http://pythonhosted.org/six>`` Python 2/3 compatibility support
+package to make the compatibility patches simpler. Where a solution provided by
+``six`` can not be used, explicitly explain the reason why in a related code
+comment.
 
-    * For ``pytest`` 2.4.1 with ``py`` library version 1.4.15 the following data
-      was copied.
+Do not use ``u"..."`` Python unicode literals since we wish to support Python
+3.1 & 3.2 versions which do not support them. Useful site for easily converting
+unicode strings to their ``unicode-escape`` encoded representation which can
+then be used with the ``six.u()`` helper function:
 
-      * Folders::
+  http://www.rapidmonkey.com/unicodeconverter
 
-          _pytest
-          argparse-1.2.1-py2.4.egg-info
-          py
-          py-1.4.15-py2.4.egg-info
-          pytest-2.4.1-py2.4.egg-info
 
-      * Files::
+EXTERNAL DOCUMENTATION
+=================================================
 
-          argparse.py
-          pytest.py
+* SOAP
 
-Python 2.4.x
+  * http://www.w3.org/TR/soap
 
-* Can not run ``pip`` using ``python.exe -m pip``. Workaround is to use one of
-  the ``pip`` startup scripts found in the Python installation's ``Scripts``
-  folder or to use the following invocation::
+  * Version 1.1.
 
-    py244 -c "import pip;pip.main()" <regular-pip-options>
+    * http://www.w3.org/TR/2000/NOTE-SOAP-20000508
 
-* ``pip``
+  * Version 1.2.
 
-  * 1.1 - last version supporting Python 2.4.
+    * Part0: Primer
 
-    * Install using::
+      * http://www.w3.org/TR/2007/REC-soap12-part0-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part0-20070427-errata.html
 
-        py244 -m easy_install pip==1.1
+    * Part1: Messaging Framework
 
-  * Can not be run using ``python.exe -m pip``.
+      * http://www.w3.org/TR/2007/REC-soap12-part1-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part1-20070427-errata.html
 
-    * Workaround is to use one of the ``pip`` startup scripts found in the
-      Python installations ``Scripts`` folder or the following invocation::
+    * Part2: Adjuncts
 
-        py244 -c "import pip;pip.main()" <regular-pip-options>
+      * http://www.w3.org/TR/2007/REC-soap12-part2-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part2-20070427-errata.html
 
-* ``pytest``
+    * Specification Assertions and Test Collection
 
-  * 2.4.1 - last version supporting Python 2.4.
+      * http://www.w3.org/TR/2007/REC-soap12-testcollection-20070427
+      * Errata:
+        http://www.w3.org/2007/04/REC-soap12-testcollection-20070427-errata.html
 
-    * Install using::
+* WS-I Basic Profile 1.1
 
-        py244 -c "import pip;pip.main()" install pytest==2.4.1
+  * http://www.ws-i.org/Profiles/BasicProfile-1.1.html
 
-  * Depends on the ``py`` package library version >= 1.4.16. However those
-    versions fail to install with Python 2.4 (tested up to and including
-    1.4.18).
+* WSDL 1.1
 
-    * May be worked around by forcing ``pytest`` to use an older ``py`` package
-      library version:
+  * http://www.w3.org/TR/wsdl
 
-      1. Run the ``pytest`` installation using ``pip``. It will fail but it will
-         install everything needed except the ``py`` package library.
-      #. Install the ``py`` package library version 1.4.15 using::
+* XML Schema
 
-           py244 -c "import pip;pip.main()" install py==1.4.15
+  * Part 0: Primer Second Edition - http://www.w3.org/TR/xmlschema-0
 
-    * If worked around by using the ``py`` 1.4.15 library version, ``pytest``'s
-      startup scripts will not work (as they explicitly check ``pytest``'s
-      package dependencies), but ``pytest`` can still be run using::
+    * Non-normative document intended to provide an easily readable description
+      of the XML Schema facilities, and is oriented towards quickly
+      understanding how to create schemas using the XML Schema language.
 
-        py244 -m pytest <regular-pytest-options>
+  * Part 1: Structures - http://www.w3.org/TR/xmlschema-1
+  * Part 2: Datatypes - http://www.w3.org/TR/xmlschema-2
 
 
 STANDARDS CONFORMANCE
@@ -541,3 +726,57 @@ WSDL XSD schema interpretation
 
     * Such same-named values break other web service related tools as well, e.g.
       WSDL analyzer & invoker at `<http://www.validwsdl.com>`_.
+
+
+PROJECT IMPLEMENTATION NOTES
+=================================================
+
+Sometimes we have a reason for implementing a feature in a certain way that may
+not be obvious at first and which thus deserves an implementation comment
+explaining the rationale behind it. In cases when such rationale would then be
+duplicated at different places in code, and project implementation note should
+be added and identified here, and its respective implementation locations marked
+using a comment such as::
+
+  # See 'Project implementation note #42'.
+
+Project implementation note #1
+-------------------------------
+``pytest`` test parametrizations must be defined so they get ordered the same in
+different test processes.
+
+Doing otherwise may confuse the ``pytest`` ``xdist`` plugin used for running
+parallel tests using multiple test processes (last tested using
+``pytest 2.5.2``, ``xdist 1.10`` & ``execnet 1.2.0``) and may cause it to exit
+with errors such as::
+
+  AssertionError: Different tests were collected between gw1 and gw0
+
+Specifically, this means that ``pytest`` test parametrizations should not be
+constructed using iteration over unordered collections such as sets or
+dictionaries, at least not with Python's hash randomization feature enabled
+(implemented as optional since Python 2.6.8, enabled by default since Python
+3.3).
+
+See the following ``pytest`` issues for more detailed information:
+
+* `#301 <http://bitbucket.org/hpk42/pytest/issue/301>`_ - serializing collection
+  process (per host) on xdist to avoid conflicts/collection errors
+* `#437 <http://bitbucket.org/hpk42/pytest/issue/437>`_ - different tests
+  collected on two nodes with xdist
+
+
+REPRODUCING PROBLEMATIC USE CASES
+=================================================
+
+Failing web service processing examples can be easily packaged as reproducible
+test cases using the suds library 'message & reply injection' technique.
+
+Some things you can achieve using this technique (for examples, see existing
+project unit tests):
+
+* Create a client object based on a fixed WSDL string.
+* Have a client object send a fixed request string without having it construct
+  one based on the loaded WSDL schema and received arguments.
+* Have a client object process a fixed reply string without having it send a
+  request to an actual external web service.
diff --git a/MANIFEST.in b/MANIFEST.in
index a6a110c..55d2b6d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,13 +1,41 @@
-#   Additional files to be included in the source distribution package (created
+# Additional files to be included in the source distribution package (created
 # by running 'setup.py sdist'). Theoretically we could avoid having to manually
 # maintain this list by using a setuptools plugin that would automatically
 # include all files under Mercurial version control, but the setuptools_hg we
 # tried out did not work correctly with Python 3.
 
 # Top level project files.
-include notes/*.rst
-include notes/*.txt
 include ez_setup.py
+include ez_setup_1_4_2.py
 include HACKING.rst
 include LICENSE.txt
 include TODO.txt
+
+# Notes.
+include notes/*.rst
+include notes/*.txt
+
+# Tests.
+recursive-include tests *.py
+
+# Project development & setup tools.
+include tools/*.cmd
+include tools/*.py
+include tools/*.txt
+recursive-include tools/suds_devel *.py
+
+# Python 2 versions prior to some early 2.7.x release and Python 3 versions
+# prior to some 3.2.x release had buggy disutils implementations that can
+# result in our project's source distribution containing some extra unwanted
+# files picked up from some of our local cache folders. This is a 3-layer fix
+# to work around the problem:
+#   1. We prune those folders just in case some of their content got added by
+#      mistake.
+#   2. An extra include is here to silence distutils warnings in case the used
+#      distutils implementation is not buggy and therefore no extra files have
+#      been added and distutils can not find anything to prune.
+#   3. To make the include actually include an existing file, setup.py
+#      constructs at least one such file to be included with a buggy distutils
+#      implementation.
+include tools/__*/*
+prune tools/__*
diff --git a/PKG-INFO b/PKG-INFO
index d4546ee..253bc0b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,47 +1,47 @@
-Metadata-Version: 1.1
-Name: suds-jurko
-Version: 0.6
-Summary: Lightweight SOAP client (Jurko's fork)
-Home-page: http://bitbucket.org/jurko/suds
-Author: Jurko Gospodnetic
-Author-email: jurko.gospodnetic at pke.hr
-License: (specified using classifiers)
-Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.6.tar.bz2
-Description: 
-        ---------------------------------------
-        Lightweight SOAP client (Jurko's fork).
-        ---------------------------------------
-        
-          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
-        dot com) hosted at 'http://fedorahosted.org/suds'.
-        
-          'Suds' is a lightweight SOAP-based web service client for Python
-        licensed under LGPL (see the LICENSE.txt file included in the
-        distribution).
-        
-          This is hopefully just a temporary fork of the original suds Python
-        library project created because the original project development seems
-        to have stalled. Should be reintegrated back into the original project
-        if it ever gets revived again.
-        
-        
-Keywords: SOAP,web,service,client
-Platform: (specified using classifiers)
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
-Classifier: Natural Language :: English
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.4
-Classifier: Programming Language :: Python :: 2.5
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.0
-Classifier: Programming Language :: Python :: 3.1
-Classifier: Programming Language :: Python :: 3.2
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Topic :: Internet
-Obsoletes: suds
+Metadata-Version: 1.1
+Name: suds-jurko
+Version: 0.7.dev0
+Summary: Lightweight SOAP client (Jurko's fork)
+Home-page: http://bitbucket.org/jurko/suds
+Author: Jurko Gospodnetić
+Author-email: jurko.gospodnetic at pke.hr
+License: (specified using classifiers)
+Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.7.dev0.tar.bz2
+Description: 
+        ---------------------------------------
+        Lightweight SOAP client (Jurko's fork).
+        ---------------------------------------
+        
+          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
+        dot com) hosted at 'http://fedorahosted.org/suds'.
+        
+          'Suds' is a lightweight SOAP-based web service client for Python
+        licensed under LGPL (see the LICENSE.txt file included in the
+        distribution).
+        
+          This is hopefully just a temporary fork of the original suds Python
+        library project created because the original project development seems
+        to have stalled. Should be reintegrated back into the original project
+        if it ever gets revived again.
+        
+        
+Keywords: SOAP,web,service,client
+Platform: (specified using classifiers)
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.4
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.1
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Topic :: Internet
+Obsoletes: suds
diff --git a/README.rst b/README.rst
index b506cbf..be3a5aa 100644
--- a/README.rst
+++ b/README.rst
@@ -54,6 +54,19 @@ Here are the basic instructions for 3 different installation methods:
 Installation troubleshooting:
 -----------------------------
 
+* Released prior to ``0.7`` have many known installation issues requiring the
+  target Python environment to be manually prepared when using some ancient
+  Python versions, e.g. 2.4, 2.5 or 3.1.
+* Releases ``0.4.1. jurko 5 < x <= 0.6`` may not be installed using ``pip`` into
+  a Python environment with an already installed ``setuptools`` package older
+  than the version expected by our project. Displayed error message includes
+  instructions on how to manually upgrade the installed ``setuptools`` package
+  before rerunning our installation.
+
+  * ``pip`` internally imports existing ``setuptools`` packages before running
+    our setup, thus preventing us from upgrading the existing ``setuptools``
+    installation inplace.
+
 * If automated ``setuptools`` Python package installation fails (used in
   releases ``0.4.1 jurko 5`` and later), e.g. due to PyPI web site not being
   available, user might need to install it manually and then rerun the
@@ -69,6 +82,333 @@ Installation troubleshooting:
 Release notes
 =================================================
 
+version 0.7 (development)
+-------------------------
+
+* Based on revision 712 (1e48fd79a1fc323006826439e469ba7b3d2b5a68) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+, except for the following versions:
+
+    * Python 3.0.x - not supported by setuptools, pip or pytest.
+
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Tested in the following environments:
+
+    * Python 2.4.3/x86 on Windows 7/SP1/x64.
+    * Python 2.4.4/x86 on Windows 7/SP1/x64.
+    * Python 2.5.4/x86 on Windows 7/SP1/x64.
+    * Python 2.5.4/x64 on Windows 7/SP1/x64.
+    * Python 2.6.6/x86 on Windows 7/SP1/x64.
+    * Python 2.6.6/x64 on Windows 7/SP1/x64.
+    * Python 2.7.6/x86 on Windows 7/SP1/x64.
+    * Python 2.7.6/x64 on Windows 7/SP1/x64.
+    * Python 2.7.7/x86 on Windows 7/SP1/x64.
+    * Python 2.7.7/x64 on Windows 7/SP1/x64.
+    * Python 3.1.3/x64 on Windows 7/SP1/x64.
+    * Python 3.2.5/x86 on Windows 7/SP1/x64.
+    * Python 3.2.5/x64 on Windows 7/SP1/x64.
+    * Python 3.3.3/x86 on Windows 7/SP1/x64.
+    * Python 3.3.3/x64 on Windows 7/SP1/x64.
+    * Python 3.3.5/x86 on Windows 7/SP1/x64.
+    * Python 3.3.5/x64 on Windows 7/SP1/x64.
+    * Python 3.4.0/x86 on Windows 7/SP1/x64.
+    * Python 3.4.0/x64 on Windows 7/SP1/x64.
+    * Python 3.4.1/x86 on Windows 7/SP1/x64.
+    * Python 3.4.1/x64 on Windows 7/SP1/x64.
+
+* Improved support for ``decimal`` XSD types.
+
+  * Now modeled internally using Python's ``decimal.Decimal`` type instead of
+    ``float`` - see the new ``suds.xsd.sxbuiltin.XDecimal`` class.
+  * Based on a patch included with `#454
+    <http://fedorahosted.org/suds/ticket/454>`_ for the original ``suds``
+    library implementation.
+  * In order to get a ``decimal`` value formatted correctly in constructed SOAP
+    request XML documents, pass it to ``suds`` as ``decimal.Decimal`` or an
+    ``int``/``long``.
+
+    * In general, passing a value of a Python type other than
+      ``decimal.Decimal`` causes that type's native string representation to be
+      used which might not strictly match the lexical representation rules
+      defined in the XSD specification for the ``decimal`` XSD type. For
+      instance, a ``float`` value may be represented using scientific notation,
+      or a ``fractions.Fraction`` may be represented using its ``numerator`` &
+      ``denominator`` values.
+    * Specific user applications can easily register their own customized
+      ``XDecimal`` implementation using ``suds.xsd.sxbuiltin.Factory.maptag()``
+      if they want to use more specialized ``decimal`` value handling.
+
+* Updated how ``suds`` constructs its cached WSDL & XML identifiers to allow
+  cached data reuse between different processes with Python's hash randomization
+  feature enabled.
+
+  * Previously constructed using the built-in Python ``hash()`` function, while
+    now it gets constructed using ``md5`` hash.
+  * Python's hash randomization (implemented since Python 2.6.8, enabled by
+    default since Python 3.3) was causing different processes to mangle their
+    cached data names differently.
+  * Many thanks to Eugene Yakubovich for reporting the issue as well as
+    providing the initial fix.
+
+* Made ``suds`` no longer eat up, log & ignore exceptions raised from registered
+  user plugins (detected & reported by Ezequiel Ruiz & Bouke Haarsma, patch &
+  test case contributed by Bouke Haarsma).
+* Fixed places in code where ``suds`` could eat up & silently ignore internal
+  Python exceptions like ``KeyboardInterrupt`` or ``SystemExit``.
+* Fixed the exception message used when attempting to construct a
+  ``suds.sax.element.Element`` with a non-``Element`` parent.
+* ``suds.xsd.sxbase.SchemaObject.content()`` now runs in linear instead of
+  quadratic time.
+* ``DepList`` class replaced with a simple ``dependency_sort()`` function taking
+  a single dependency dictionary as input.
+
+  * The original implementation's interface was too heavy-weight with no added
+    value.
+  * Anything tried with the original interface outside the basic use-case
+    covered by ``dependency_sort()`` was actually or could be easily broken.
+  * ``suds.xsd.deplist`` module renamed to ``suds.xsd.depsort``.
+
+* A referencing XSD element's ``form`` value now read correctly from the
+  referenced element.
+
+  * Many thanks to Andrew Yager from BitBucket for reporting the issue.
+
+* ``suds.cache`` module cleanup.
+
+  * Fixed ``FileCache`` default cache location related security issue.
+
+    * Each process now uses a separate temporary folder as its default cache
+      location.
+    * Different ``FileCache`` instances within the same process still use the
+      same default cache location and user may still explicitly specify a
+      non-default location for each ``FileCache`` instance.
+    * Default cache location now gets removed automatically on process exit.
+      User code may disable this removal by setting the
+      ``FileCache.remove_default_location_on_exit`` class attribute to False.
+    * Additional external information on this issue:
+
+      * `Red Hat bug 978696
+        <https://bugzilla.redhat.com/show_bug.cgi?id=978696>`_
+      * `CVE-2013-2217
+        <http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-2217>`_
+      * `Ubuntu USN-2008-1: Suds vulnerability
+        <http://www.ubuntu.com/usn/USN-2008-1>`_
+      * http://lists.opensuse.org/opensuse-updates/2013-07/msg00062.html
+      * https://bugzilla.novell.com/show_bug.cgi?id=827568
+      * http://www.openwall.com/lists/oss-security/2013/06/27/8
+
+    * Many thanks to Rolf Krahl for the initial report, providing links to
+      related external resources as well as helping brainstorm the whole issue.
+
+  * Fixed a bug causing ``DocumentCache`` to never actually cache any documents
+    since one of the last commits made to the original suds project.
+
+    * That commit refactored ``suds.sax.document.Document`` so it is no longer
+      derived from ``suds.sax.element.Element`` while the
+      ``suds.cache.DocumentCache.put()`` implementation simply did nothing when
+      passed something other than a ``suds.sax.element.Element`` instance.
+      ``suds.reader.DocumentReader`` on the other hand always passes
+      ``suds.sax.document.Document`` instances to its cache's ``put()`` method.
+    * Many thanks to bgr\_ at BitBucket for reporting the issue.
+
+  * Fixed a bug causing ``DocumentCache`` & ``ObjectCache`` to not remove their
+    cached files when failing to read data from them or process the data read
+    from them.
+  * ``FileCache`` version file operations now take care to close the file in
+    case of a failed read/write operation.
+  * Removed ``FileCache.setlocation()`` method as it was never used inside
+    ``suds`` and if used from user code would have caused the cache to use a
+    specific folder but without making sure that the data already stored in it
+    has been prepared for the correct ``suds`` version, as done when passing a
+    location parameter to the ``FileCache`` constructor.
+  * Private and protected ``FileCache`` interface methods renamed to use leading
+    underscores.
+
+    * ``FileCache.getf()`` --> ``FileCache._getf()``.
+    * ``FileCache.__fn()`` --> ``FileCache.__filename()``.
+    * ``FileCache.checkversion()`` --> ``FileCache.__check_version()``.
+    * ``FileCache.mktmp()`` --> ``FileCache.__mktmp()``.
+    * ``FileCache.open()`` --> ``FileCache.__open()``.
+    * ``FileCache.setduration()`` --> ``FileCache.__set_duration()``.
+    * ``FileCache.validate()`` --> ``FileCache.__remove_if_expired()``.
+
+  * Updated ``FileCache`` duration implementation.
+
+    * ``FileCache`` construction now takes standard ``datetime.timedelta``
+      duration related keyword arguments instead of just ``weeks``, ``days``,
+      ``hours``, ``minutes`` & ``seconds``. More to the point, it now also
+      supports ``milliseconds`` & ``microseconds`` keyword arguments.
+    * Corrected ``FileCache`` docstring stating that it accepted a ``months``
+      keyword argument. Using that argument would actually have caused a failure
+      when passing it to a ``datetime.timedelta`` initializer internally.
+    * You may now specify multiple duration keyword arguments in ``FileCache``
+      construction and they will all get summed up when constructing the
+      internal ``datetime.timedelta`` duration representation. Before, you could
+      specify such multiple arguments, but that would only make the
+      ``FileCache`` silently use duration ``0``, i.e. its cache entries would
+      never expire.
+
+* Fixed ``suds.sax.document.Document`` str conversion broken around the end of
+  2011 by some accidental interaction between our Python 3 compatibility fixes
+  and one of the final official suds project commits making
+  ``suds.sax.document.Document`` no longer be derived from
+  ``suds.sax.element.Element``.
+
+  * Many thanks to Ezequiel Ruiz (emruiz81 at BitBucket) for detecting &
+    reporting the issue, as well as providing the initial patch.
+
+* Cleaned up ``suds.transport`` ASCII/unicode URL/data handling.
+
+  * ``suds.transport.Request`` now allows specifying its URL input as either a
+    byte or a unicode string with any Python version. Internally that URL
+    information is always converted to the used Python interpreter's native
+    ``str`` data type (byte string for Python versions prior to 3.0, or unicode
+    string for later ones).
+  * Given URLs must not contain any non-ASCII characters, and any attempt to
+    create a ``suds.transport.Request`` with such an invalid URL is reported as
+    a ``UnicodeError`` (either ``UnicodeDecodeError`` or ``UnicodeEncodeError``
+    depending on the exact Python version and the given URL data type used).
+  * ``suds.transport.Reply`` & ``suds.transport.Request`` string representation
+    cleaned up and no longer raises an error when their message data contains
+    non-ASCII characters.
+
+* ``suds.client`` module cleanup.
+
+  * Removed unused ``suds.client.Client.messages`` attribute.
+  * Renamed private ``SoapClient`` & ``SimClient`` classes:
+
+    * ``SoapClient`` --> ``_SoapClient``.
+    * ``SimClient`` --> ``_SimClient``.
+
+  * Several private methods renamed:
+
+    * ``_SoapClient.location()`` --> ``_SoapClient.__location()``.
+    * ``_SoapClient.get_fault()`` --> ``_SoapClient.__get_fault()``.
+    * ``_SoapClient.headers()`` --> ``_SoapClient.__headers()``.
+
+  * ``RequestContext`` no longer has ``client`` & ``original_envelope``
+    attributes.
+
+    * ``client`` attribute seems unnecessary.
+    * ``original_envelope`` was an incorrectly documented bug trap - it
+      represented the XML request envelope as a ``SAX`` XML document from after
+      being processed by registered ``marshalled`` plugins, but before being
+      processed by registered ``sending`` plugins. Users should use the
+      ``envelope`` attribute instead which can easily be converted into a
+      ``SAX`` XML document if needed by parsing it using
+      ``suds.sax.parser.Parser.parse()``. That envelope has been consistently
+      processed by all relevant registered plugins and matches the data to be
+      sent over the registered transport exactly.
+
+  * Cleaned up ``_SoapClient`` debug log messages a bit.
+
+* ``suds.reader`` module cleanup.
+
+  * Several private methods renamed:
+
+    * ``DocumentReader.cache()`` --> ``DocumentReader.__cache()``
+    * ``DocumentReader.download()`` --> ``DocumentReader.__fetch()``
+    * ``DefinitionsReader.cache()`` --> ``DefinitionsReader.__cache()``
+
+* Updated the ``BuildError`` exception message.
+
+  * Reformatted.
+  * Converted to a unicode string.
+
+* ``suds.binding.Binding`` converted to a new-style class.
+* ``suds.tostr()`` utility function may no longer silently eat internal Python
+  exceptions like ``KeyboardInterrupt`` or ``SystemExit``.
+* Removed the unused ``SoapHeadersNotPermitted`` exception class.
+* Extra input arguments now reported when invoking web service operations taking
+  no input parameters.
+* Using injected requests/replies/error-information with a web service operation
+  taking at least one input parameter no longer causes suds to report an invalid
+  extra argument error.
+
+* Internal project development improvements.
+
+  * The project will from now on be distributed as a wheel as well as a source
+    distribution.
+  * Added a script for automatically setting up required development Python
+    environments for this project, hopefully supporting the full range of
+    supported Python versions out of the box.
+  * Improved internal project ``HACKING.rst`` documentation.
+
+* ``setup.py`` improvements.
+
+  * Python 3.0.x releases explicitly marked as not supported.
+  * Attempting to run ``setup.py`` in an unsupported Python environment now
+    reports a clean error message.
+  * Now uses ``setuptools`` 1.4.2 with Python 2.4 & 2.5, and ``setuptools``
+    5.1 with all more recent Python releases.
+  * Project may now be installed without even in environments when you can not
+    install ``setuptools``.
+
+    * In such cases ``setup.py`` will attempt to use any preinstalled
+      ``setuptools`` version, and if none is available, it will disable some of
+      its features and fall back to using a plain ``distutils`` based setup. See
+      the ``setup.py`` script comments for a more detailed listing of all
+      ``setup.py`` features affected by this.
+
+  * Several installation issues fixes when installing into Python 3.x
+    environments prior to Python 3.2.3.
+  * When installing the project into a Python 3.x environment prior to Python
+    3.2, ``setuptools`` is not installed automatically since one of its test
+    modules contains UTF-8 BOM characters, which would cause such automated
+    installation to fail.
+
+    * If needed, ``setuptools`` can still be installed into such environments by
+      manually running its ``ez_setup.py`` installation script. Such an
+      installation will encounter the same errors but will ignore them,
+      effectively just leaving the installed ``setuptools`` package with one
+      defective test module, but fully operational at run-time.
+
+  * When installing the project into a Window Python 2.5 environment, you no
+    longer need to manually install a compatible ``colorama`` package versions
+    in order to be able to run the project tests.
+  * Package meta-data may now contain non-ASCII characters on platforms where
+    that is allowed, namely with all Python versions except Python 3.x prior to
+    3.2.2.
+  * ``setup.py test`` command improvements.
+
+    * Now works in Python 2.4.x environments.
+    * Now reports cleanly if it can not be used for some reason, both when run
+      and in the command's ``--help-commands`` listing.
+    * Better commented the related implementation.
+
+* Test suite improvements.
+
+  * Test suite no longer installed together with the project, thus no longer
+    causing confusion by existing in the target Python environment as a global
+    ``tests`` package.
+
+    * The tests may now be run from the source archive, and will always run on
+      the suds version found installed in the used Python environment.
+
+  * Refactored the quick & dirty batch script used to run all the project tests
+    in multiple Python environments to remove much code duplication.
+  * Automated project testing in several additional Python environment versions.
+  * Added more detailed XSD modeling tests.
+  * Added tests demonstrating how additional or replacement built-in XSD types
+    can be registered with suds.
+  * All project tests now using Python 2 & 3 compatible source code and so no
+    longer need to be built separately for Python 3.
+  * Added new and updated existing ``suds.cache`` module related tests.
+  * Documented that all ``pytest`` test parametrizations should be prepared so
+    they get ordered the same on all test runs. See ``Project implementation
+    note #1`` in ``HACKING.rst`` for more detailed information.
+
+    * Many thanks to Bruno Oliveira (nicoddemus at BitBucket) for researching
+      related ``pytest`` ``xdist`` usage problems, discovering & explaining the
+      underlying issue as well as providing an initial project patch for it.
+
 version 0.6 (2014-01-24)
 -------------------------
 
@@ -183,6 +523,17 @@ version 0.6 (2014-01-24)
     * Now return an exit code indicating the test result (0=success,
       !0=failure).
 
+* Known defects.
+
+  * Extra argument errors not reported for web service operations taking no
+    input parameters.
+  * Invalid extra argument error reported when using an injected request/reply/
+    error-information with a web service operation taking at least one input
+    parameter.
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.5 (2013-11-25)
 ------------------------
 
@@ -241,6 +592,12 @@ version 0.5 (2013-11-25)
   the project's source distribution package.
 * Better documented the project's development & testing environment.
 
+* Known defects.
+
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.4.1 jurko 5 (2013-11-11)
 ----------------------------------
 
@@ -339,9 +696,9 @@ version 0.4.1 jurko 5 (2013-11-11)
 
 * Added ``unwrap`` option, allowing the user to disable ``suds`` library's
   automated simple document interface unwrapping (contributed by Juraj Ivančić).
-* Fixed a problem with ``suds`` constructing parameter XML elements in its SOAP
-  requests in incorrect namespaces in case they have been defined by XSD schema
-  elements referencing XSD schema elements with a different target namespace.
+* SOAP request parameter XML elements no longer constructed in incorrect
+  namespaces in case they have been defined by XSD schema elements referencing
+  XSD schema elements with a different target namespace.
 * ``DocumentStore`` instance updated.
 
   * Separate ``DocumentStore`` instances now hold separate data with every
@@ -388,6 +745,12 @@ version 0.4.1 jurko 5 (2013-11-11)
   * Removed unused ``Cache`` module ``getf()`` & ``putf()`` functions.
     ``getf()`` left only in ``FileCache`` and its derived classes.
 
+* Known defects.
+
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.4.1 jurko 4 (2012-04-17)
 ----------------------------------
 
@@ -418,6 +781,15 @@ version 0.4.1 jurko 4 (2012-04-17)
   while its name inferred that 'it has no upper limit on its number of
   occurrences'.
 
+* Known defects.
+
+  * SOAP request parameter XML elements constructed in incorrect namespaces in
+    case they have been defined by XSD schema elements referencing XSD schema
+    elements with a different target namespace.
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.4.1 jurko 3 (2011-12-26)
 ----------------------------------
 
@@ -449,6 +821,15 @@ version 0.4.1 jurko 3 (2011-12-26)
   with the parameter ``nobuiltin=True``.
 * Added more test cases.
 
+* Known defects.
+
+  * SOAP request parameter XML elements constructed in incorrect namespaces in
+    case they have been defined by XSD schema elements referencing XSD schema
+    elements with a different target namespace.
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.4.1 jurko 2 (2011-12-24)
 ----------------------------------
 
@@ -479,6 +860,15 @@ version 0.4.1 jurko 2 (2011-12-24)
     exception due to the code in question making some undocumented assumptions
     on how the build information string should be formatted.
 
+* Known defects.
+
+  * SOAP request parameter XML elements constructed in incorrect namespaces in
+    case they have been defined by XSD schema elements referencing XSD schema
+    elements with a different target namespace.
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
+
 version 0.4.1 jurko 1 (2011-12-24)
 ----------------------------------
 
@@ -555,6 +945,12 @@ version 0.4.1 jurko 1 (2011-12-24)
 
   * Converting a ``suds.client.Client`` object to a string fails & raises an
     ``IndexError`` exception.
+  * SOAP request parameter XML elements constructed in incorrect namespaces in
+    case they have been defined by XSD schema elements referencing XSD schema
+    elements with a different target namespace.
+  * Security issue CVE-2013-2217 - using fixed default cache location.
+  * Incorrect referencing XSD element's ``form`` attribute value handling - read
+    directly instead of from the references XSD element.
 
 
 Original suds library release notes
@@ -563,6 +959,9 @@ Original suds library release notes
 **version 0.4.1 (2010-10-15)**
 
 * <undocumented>
+* Known defects.
+
+  * Security issue CVE-2013-2217 - using fixed default cache location.
 
 **version 0.4 (2010-09-08)**
 
diff --git a/TODO.txt b/TODO.txt
index 5a0ee1e..39e89d4 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,4 +1,4 @@
-PRIORETIZED:
+PRIORITIZED:
 =================================================
 
 (21.12.2011.)
@@ -8,12 +8,12 @@ PRIORETIZED:
 (+)     * Document how to access this forked project's development sources &
 (+)       released files.
 (+)         * Jurko's Mercurial repository hosted at BitBucket and accessible
-(+)           from 'https://bitbucket.org/jurko/suds'.
+(+)           from 'http://bitbucket.org/jurko/suds'.
 (+)             * Already documented in README.
 (+)             * Add more details to HACKING.
 (+)         * Suds library Python 3 patches hosted in a Mercurial patch queue
 (+)           repository at BitBucket and accessible from
-(+)           'https://bitbucket.org/bernh/suds-python-3-patches'.
+(+)           'http://bitbucket.org/bernh/suds-python-3-patches'.
 (+)             * Already documented in HACKING.
 (+) * (Jurko) Minor stylistic changes & typo corrections.
 (+)     * Code.
@@ -30,7 +30,7 @@ PRIORETIZED:
 (+)         * 'dependancies' --> 'dependencies'.
 (+)         * 'imcoming' --> 'incoming'.
 (+)         * 'relavent' --> 'relevant'.
-(+)         * 'indicat' --> 'inidcat'.
+(+)         * 'inidcat' --> 'indicat'.
 
 (22.12.2011.)
 
@@ -96,18 +96,18 @@ PRIORETIZED:
 (+)   inside a choice.
 (+)     * Test scenario (syntax not precise).
 (+)         <choice>
-(+)             <element "a" - string />
+(+)             <element "a" - string/>
 (+)             <element "s">
 (+)                 <sequence>
-(+)                     <element "s1" - string />
-(+)                     <element "s2" - string />
+(+)                     <element "s1" - string/>
+(+)                     <element "s2" - string/>
 (+)                 </sequence>
 (+)             </element>
 (+)         </choice>
 (+)     * When 's' is None and 'a' is not - 'a' should be used.
 (+)     * When 'a' is None and 's' is not - 's' should be used.
-(+)     * When 's' is used, all of its subelements should be used independent of
-(+)       whether they are None or not.
+(+)     * When 's' is used, all of its child elements should be used independent
+(+)       of whether they are None or not.
 (+)     * Add related test.
 (+) * (Jurko) Prepare the '0.4.1 jurko 1' release.
 (+)     * Follow the documented release procedure.
@@ -309,7 +309,7 @@ PRIORETIZED:
 (01.03.2013.)
 
 (+) * (Jurko) Sync with external related repositories.
-(+)     * 'https://bitbucket.org/palday/suds'.
+(+)     * 'http://bitbucket.org/palday/suds'.
 
 (27.03.2013.)
 
@@ -359,8 +359,8 @@ PRIORETIZED:
 (+) * (Jurko) Reorganize SimClient injection keywords.
 (+)     * 'msg' - request message.
 (+)     * 'reply' - reply message ('msg' must not be set).
-(+)     * 'status' - HTTP status code acompanying the 'reply' message.
-(+)     * 'description' - description string acompanying the 'reply' message.
+(+)     * 'status' - HTTP status code accompanying the 'reply' message.
+(+)     * 'description' - description string accompanying the 'reply' message.
 (+) * (Jurko) Check failing tests.
 (+)     * All tests now pass except for ones related to SOAP Fault unicode
 (+)       faultstring processing.
@@ -368,7 +368,7 @@ PRIORETIZED:
 (29.03.2013.)
 
 (+) * (Jurko) Sync with external related repositories.
-(+)     * 'https://bitbucket.org/blarghmatey/suds-blarghmatey'.
+(+)     * 'http://bitbucket.org/blarghmatey/suds-blarghmatey'.
 (+) * (Jurko) Additional SOAP web service reply tests.
 (+) * (Jurko) Fix detected unicode problems.
 (+)     * Remove invalid WebFault fix merged from an external source.
@@ -384,7 +384,7 @@ PRIORETIZED:
 (+)           which would then get reported as 'document not found'.
 (+)     * SAX parser now accepts only byte string content instead of also
 (+)       accepting unicode strings containing latin1 characters only.
-(+)     * Make tests now specify their fixed wsdl & reply content as byte
+(+)     * Make tests now specify their fixed WSDL & reply content as byte
 (+)       strings only.
 (+)     * Make all tests pass.
 (+)         * Python 2.4.
@@ -499,10 +499,10 @@ PRIORETIZED:
 (18.11.2013.)
 
 (+) * (Jurko) Fix suds time-zone handling according to a pull request received
-(+)   on bitbucket from MDuggan1.
+(+)   on BitBucket from MDuggan1.
 (+)     * Research.
 (+)         * Suds assumes that all timezones have a full-hour offset from the
-(+)           UTC timeone and does not work correctly with those that do not.
+(+)           UTC timezone and does not work correctly with those that do not.
 (+)             * This seems to be a suds specific problem and not a more
 (+)               general Python issue as some information on the net implies.
 (+)         * FixedOffsetTimezone.
@@ -516,7 +516,7 @@ PRIORETIZED:
 (19.11.2013.)
 
 (+) * (Jurko) Fix suds time-zone handling according to a pull request received
-(+)   on bitbucket from MDuggan1.
+(+)   on BitBucket from MDuggan1.
 (+)     * Research.
 (+)     * Prepare date/time string parsing tests.
 
@@ -524,7 +524,7 @@ PRIORETIZED:
 
 (+) * (Jurko) Start documenting the upcoming suds 0.4.1 jurko 6 release.
 (+) * (Jurko) Fix suds time-zone handling according to a pull request received
-(+)   on bitbucket from MDuggan1.
+(+)   on BitBucket from MDuggan1.
 (+)     * Research.
 (+)     * Implement parsing.
 (+)     * DateTime no longer derived from Date & Time.
@@ -553,7 +553,7 @@ PRIORETIZED:
 (21.11.2013.)
 
 (+) * (Jurko) Fix suds time-zone handling according to a pull request received
-(+)   on bitbucket from MDuggan1.
+(+)   on BitBucket from MDuggan1.
 (+)     * Check for feedback from users requesting this patch.
 (+)     * Add tests.
 (+)         * FixedOffsetTimezone class.
@@ -706,7 +706,7 @@ PRIORETIZED:
 (+) * (Jurko) See if the suds HttpTransport.open() method ever gets called.
 (+)     * Yup, transport open() methods get called from DocumentReader, e.g.
 (+)       when downloading a WSDL schema from the net.
-(+)     * It seems like Transport's open() & send() methods might be megreable,
+(+)     * It seems like Transport's open() & send() methods might be mergeable,
 (+)       but that would first require further research. For now - YAGNI.
 (+) * (Jurko) Process pull requests received on 'bitbucket.org'.
 (+)     * Fix setup.py current working folder path comparison so it works with
@@ -733,7 +733,7 @@ PRIORETIZED:
 (+) * (Jurko) Remove unnecessary logger objects.
 (+) * (Jurko) Process the project issue #2 reported on BitBucket by Arthur
 (+)   Clune, related to not being able to set the option cache location if the
-(+)   default cache location is not a writeable folder.
+(+)   default cache location is not a writable folder.
 (+)     * Research.
 (+)     * Prepare tests.
 (+)         * Default cache.
@@ -774,31 +774,926 @@ PRIORETIZED:
 (+)     * Update version tag.
 (+)     * Tag in Hg.
 (+)     * Update version information.
-
-    * (Jurko) Prepare a new suds-jurko 0.6 release.
-        * Retag in Hg
-        * Package the release.
-        * Tag next version development.
-        * Distribute the new release.
-            * PyPI.
-            * BitBucket.
-            * Notify Gauthier Bastien - see comments for commit
-              9da81de891958292850a242781b30a3493f617 on BitBucket.
+(+)     * Retag in Hg
+(+)     * Package the release.
+(+)     * Distribute the new release.
+(+)         * PyPI.
+(+)         * BitBucket.
+(+)         * Notify Gauthier Bastien - see comments for commit
+(+)           9da81de891958292850a242781b30a3493f617 on BitBucket.
+(+)     * Prepare project information for the next development cycle.
 
 (25.01.2014.)
 
-    * (Jurko) Process decimal type support patch sent in by pendletongp on
-      BitBucket.
-        * Research.
-        * Prepare tests.
-        * Merge.
-        * Thorough code review.
+(+) * (Jurko) Look into issue #12 & the related pull request #22 reported on
+(+)   BitBucket - suds-jurko incorrectly returning a raw Fault() instead of a
+(+)   WebFault() object when faults=False. Reported as a regression against the
+(+)   base suds project.
+(+)     * Research.
+(+)         * Most likely introduced by commit
+(+)           '506c8362c81343f1f629906606db23daf8b426ec'.
+(+)         * Try to reproduce using an injected reply.
+(+)             * Test against the current HEAD commit.
+(+)             * Find a commit not that works correctly.
+(+)     * Status.
+(+)         * Could not reproduce the issue. Truly suds-jurko returns a
+(+)           suds.sudsobject.Fault object, but the original suds library
+(+)           implementation does not return a suds.WebFault object either.
+(+)           Further work on this can be done only once the exact desired
+(+)           behaviour is defined.
+(+)     * Ask for feedback containing a reproducible example.
+
+(26.01.2014.)
+
+(+) * (Jurko) Code cleanup.
+(+)     * Stylistic cleanup.
+(+) * (Jurko) Process decimal type support patch sent in by pendletongp on
+(+)   BitBucket.
+(+)     * Research.
+(+)         * Where a Python type to SOAP representation transformation is
+(+)           performed.
+(+)             * Expected locations.
+(+)                 * When constructing a web service operation invocation
+(+)                   request, i.e. when marshaling Python data to a SOAP
+(+)                   request XML (mx package).
+(+)         * Where a SOAP representation to Python type transformation is
+(+)           performed.
+(+)             * Expected locations.
+(+)                 * When processing a web service operation reply, i.e. when
+(+)                   unmarshaling Python data from a SOAP response XML (umx
+(+)                   package).
+
+(28.01.2014.)
+
+(+) * (Jurko) Process decimal type support patch sent in by pendletongp on
+(+)   BitBucket.
+(+)     * Prepare tests.
+(+)         * Representation tests for different data types.
+(+)         * Transformation tests for different data types.
+(+)     * Thorough code review.
+(+)     * Add XFloat tests.
+
+(29.01.2014.)
+
+(+) * (Jurko) Update XFloat to correctly translate decimal.Decimal &
+(+)   numbers.Real (on Python 2.6+) values to their XSD value representation.
+(+)   Currently, inconsistently with how other types are handled, they get
+(+)   translated to the same decimal/fraction value which then gets changed to a
+(+)   string later on when actually writing it to an XML string. This works out
+(+)   fine for decimals in the end but not for rationals/fractions.
+(+)     * Research.
+(+)         * decimal.Decimal input values already get entered correctly into
+(+)           constructed SOAP XML requests.
+(+)         * It seems not all numbers.Real derived classes can be converted to
+(+)           their XSD value representation the same way.
+(+)             * Some examples.
+(+)                 * float --> str(x)
+(+)                 * fractions.Fraction --> str(float(x))
+(+)                 * decimals.Decimal --> str(x)
+(+)                 * int --> str(x)
+(+)                 * bool --> str(int(x))
+(+)             * Adding support for them would pair down to simply listing all
+(+)               supported types explicitly and adding code for translating
+(+)               each of them separately. We can not possibly do this for 'all
+(+)               data types in the world' so the current implementation seems
+(+)               as good as any.
+(+)         * A user application requiring support for a specific type can
+(+)           always add it itself by implementing a replacement XFloat class
+(+)           and registering it using suds.xsd.xbuiltin.Factory.maptag().
+(+)     * Discard the task.
+(+)     * Add a new development note document describing XType class usage for
+(+)       built-in XSD type value translation between Python objects and their
+(+)       XSD value representations.
+(+) * (Jurko) Clean up different XType.translate() methods. See if there is any
+(+)   difference in them returning a string or any other Python object
+(+)   convertible to a string using str().
+(+)     * If there is no difference, clean up the code to simply return the
+(+)       original value where possible.
+(+) * (Jurko) Process decimal type support patch sent in by pendletongp on
+(+)   BitBucket.
+(+)     * Research.
+
+(30.01.2014.)
+
+(+) * (Jurko) Process decimal type support patch sent in by pendletongp on
+(+)   BitBucket.
+(+)     * Research.
+(+)         * XSD type value representation is incorrect.
+(+)     * Apply equivalent changes together with adding related unit tests.
+(+)     * Comment on the original issue #3 & patch request #19 on BitBucket.
+(+)     * Update release notes.
+(+)     * Later ideas to consider.
+(+)         * 'float' --> 'decimal.Decimal' conversion which has not been
+(+)           implemented in Python before Python 2.7.
+(+)             * YAGNI for now.
+(+)             * Update release notes.
+
+(31.01.2014.)
+
+(+) * (Jurko) Restore last_sent()/last_received() getters for last used SOAP
+(+)   request/response messages.
+(+)     * Research background.
+(+)         * Requested by.
+(+)             * andreebrazeau as BitBucket.
+(+)                 * 'http://bitbucket.org/andreebrazeau'.
+(+)             * FerCesar at BitBucket.
+(+)                 * 'http://bitbucket.org/FerCesar'.
+(+)             * Lucas Sampaio at BitBucket.
+(+)                 * 'http://bitbucket.org/lucassmagal'.
+(+)         * Related links.
+(+)             * 'http://stackoverflow.com/questions/4426204/
+(+)               how-can-i-output-what-suds-is-generating-receiving'.
+(+)             * 'http://jortel.fedorapeople.org/suds/doc/
+(+)               suds.client.Client-class.html'
+(+)         * Code removed in commit 'f0034d6826c179625478bc19ae2a39a0b803fc3a'.
+
+(03.02.2014.)
+
+(+) * (Jurko) Restore last_sent()/last_received() getters for last used SOAP
+(+)   request/response messages.
+(+)     * Clean up related code.
+
+(05.02.2014.)
+
+(+) * (Jurko) Clean up SoapClient/SimClient related code a bit.
+(+)     * Generic code cleanup.
+(+)     * Mark private methods using leading underscores.
+(+)     * Remove RequestContext.original_envelope attribute.
+(+)     * Stop logging incorrect 'original_envelope' value.
+(+)     * Rename to _SoapClient/_SimClient.
+
+(06.02.2014.)
+
+(+) * (Jurko) Add tests for the functionality potentially affected by a planned
+(+)   _SoapClient/_SimClient refactoring.
+(+)     * Cache - update existing tests.
+(+)         * File operation failures.
+
+(07.02.2014.)
+
+(+) * (Jurko) Update FileCache duration handling.
+(+)     * Research.
+(+)         * Currently FileCache.__set_duration() implementation allows using
+(+)           only a single unit.
+(+)         * Check whether Python 2.4 datetime.timedelta class implementation
+(+)           allows it to be constructed from multiple units.
+(+)     * Store FileCache.duration as a datetime.timedelta instance.
+(+)     * Support for specifying FileCache durations with multiple time units.
+(+)     * Add tests.
+(+)         * Default suds.Client cache.
+(+)             * Already tested in test_default_cache_construction().
+(+)                 * Class.
+(+)                 * Duration.
+(+)         * Default duration.
+(+)         * Set duration (<0, 0, >0, single/multiple duration arguments).
+(+)         * Exception on duration < 0.
+(+)             * YAGNI. Current implementation, using negative values as
+(+)               regular durations and simply adding them to a specific file
+(+)               timestamp to see whether it expired seems good enough as well
+(+)               as intuitive enough for the user.
+(+)         * Expiration after expiration time with duration > 0.
+(+)         * Not expiring after expiration time with duration = 0
+(+)         * Not expiring before expiration time with duration > 0.
+(+)         * Not expiring before expiration time with duration = 0.
+(+)         * Duration testing for all FileCache derived classes - DocumentCache
+(+)           & ObjectCache.
+(+)         * FileCache and derived classes - one item expiring should not
+(+)           affect other unexpired items.
+(+)     * Update release notes.
+(+) * (Jurko) Per process default FileCache folder randomization + removal on
+(+)   exit.
+(+)     * Research.
+(+)         * Related BitBucket issue #15.
+(+)         * Links from the BitBucket issue #15.
+(+)             * 'https://bugzilla.redhat.com/show_bug.cgi?id=978696'.
+(+)             * 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-2217'.
+
+(09.02.2014.)
+
+(+) * (Jurko) Per process default FileCache folder randomization + removal on
+(+)   exit - fix CVE-2013-2217.
+(+)     * Add tests.
+(+)     * Implement.
+(+)     * Update release notes.
+(+)     * Update related issues on BitBucket.
+(+) * (Jurko) Extract all suds.client.Client related tests related to how it
+(+)   uses its worker cache/store/transport components into separate
+(+)   test_client.py test module. Modules like test_transport.py or
+(+)   test_cache.py should concentrate on testing that specific component
+(+)   functionality and not how the component gets used by suds.client.Client.
+(+)     * Test modules to check.
+(+)         * 'test_cache.py'.
+(+)         * 'test_client_cache.py'.
+(+)         * 'test_transport.py'.
+(+)     * Several transport usage tests already prepared in test_transport_1.py
+(+)       on Jurko's notebook.
+
+(10.02.2014.)
+
+(+) * (Jurko) Add tests for the functionality potentially affected by a planned
+(+)   _SoapClient/_SimClient refactoring.
+(+)     * Client's cache/store/transport usage.
+(+)         * WSDL access.
+(+)             * If an object is found in cache it should not be looked up in
+(+)               the given document store or transported.
+(+)             * If an object is found in a given document store it should not
+(+)               be transported.
+
+(14.02.2014.)
+
+(+) * (Jurko) Clean up test_client.py test module - extract all remaining
+(+)   suds.client.Client related tests related to how it uses its worker cache/
+(+)   store/transport components.
+(+)     * Test modules to check.
+(+)         * 'test_transport_http.py'.
+(+) * (Jurko) Update test_sending_non_ascii_data_to_unicode_URL() test to test
+(+)   both send() & open() transport operations.
+
+(17.02.2014.)
+
+(+) * (Jurko) Clean up Transport ASCII/Unicode URL/data handling.
+(+)     * Design desired behaviour.
+(+)         * URL may be specified as either a byte string or a unicode string.
+(+)         * URL input data may contain ASCII characters only.
+(+)         * URL stored internally as a native str type (byte string with
+(+)           Python versions prior to 3, unicode string with Python 3+).
+(+)             * Using it in a different format breaks the underlying
+(+)               HttpTransport httplib implementation under some Python
+(+)               versions.
+(+)         * Bytes data used directly.
+(+)         * Unicode data used directly with the underlying implementation
+(+)           converting the data to bytes or raising an error.
+(+)             * HttpTransport.
+(+)                 * Python2 - converts to bytes if ASCII chars only.
+(+)                 * Python2 - error for non-ASCII chars.
+(+)                 * Python3 - error.
+(+)     * Fix suds.transport Reply & Request string representation.
+(+)     * Update suds.transport.Request.
+(+)         * Check for non-ASCII URL characters.
+(+)         * Hold it internally as a native str type.
+(+)     * Update suds.transport.http.HttpTransport.
+(+)         * Expects the suds.transport.Request to have already checked its URL
+(+)           content.
+(+)     * Document the desired design (docstrings).
+(+)         * Request.
+(+)         * Transport.
+(+)     * Update tests.
+(+)         * Request tests.
+(+)             * Bytes URL.
+(+)                 * Must be used directly on Python 2.
+(+)                 * Must be converted to str URL internally on Python 3.
+(+)                 * Error if it includes non-ASCII character codes.
+(+)             * Unicode URL.
+(+)                 * Must be converted to str URL internally on Python 2.
+(+)                 * Must be used directly on Python 3.
+(+)                 * Error if it includes non-ASCII character codes.
+(+)         * HttpTransport tests.
+(+)             * Sending data through the network.
+(+)             * Remove tests made redundant by the new Request testing.
+(+)     * Update release notes.
+
+(18.02.2014.)
+
+(+) * (Jurko) Add tests for the functionality potentially affected by a planned
+(+)   _SoapClient/_SimClient refactoring.
+(+)     * Client's cache/store/transport usage.
+(+)         * If an object is found in cache it should not be looked up in the
+(+)           given document store or transported.
+(+)             * WSDL document.
+(+)             * Imported XSD schema.
+(+)             * Included XSD schema.
+
+(19.02.2014.)
+
+(+) * (Jurko) Add a failing test illustrating a WSDL import detected when
+(+)   attempting to test importing a cached WSDL schema.
+(+)     * test_WSDL_import().
+(+) * (Jurko) Add tests for the functionality potentially affected by a planned
+(+)   _SoapClient/_SimClient refactoring.
+(+)     * Client's cache/store/transport usage.
+(+)         * If an object is found in cache it should not be looked up in the
+(+)           given document store or transported.
+(+)             * Imported WSDL document.
+(+)         * If an object is found in a given document store it should not be
+(+)           transported.
+(+)             * WSDL document.
+(+)             * Imported WSDL document.
+(+)             * Imported XSD schema.
+(+)             * Included XSD schema.
+(+)         * If an object is not cached or found in a given document store it
+(+)           should not transported.
+(+)             * WSDL document.
+(+)             * Imported WSDL document.
+(+)             * Imported XSD schema.
+(+)             * Included XSD schema.
+(+)     * Client's cache/store/transport usage.
+(+)         * Different cachingpolicy option values.
+(+)             * cachingpolicy == 0 - XML documents are cached.
+(+)                 * Already tested.
+(+)             * cachingpolicy == 1 - final WSDL objects are cached.
+(+)                 * Loading a WSDL object from cache should avoid attempting
+(+)                   to fetch any additional external data either from the
+(+)                   cache, the document store or the registered transport.
+
+(20.02.2014.)
+
+(+) * (Jurko) Add tests for the functionality potentially affected by a planned
+(+)   _SoapClient/_SimClient refactoring.
+(+)     * Transport.
+(+)         * Base Transport, Reply & Request classes already tested.
+(+)         * HttpTransport.
+(+)             * urllib2.HTTPError raised from urllib's open() operation.
+(+)                 * ACCEPT/NO_CONTENT status handling - buggy but YAGNI for
+(+)                   now.
+(+)                     * Add a separate todo item to look into this at some
+(+)                       later time.
+(+)                 * Other HTTPError exceptions.
+(+)             * Other functionality already tested well enough.
+(+)     * _SoapClient - currently can only be tested via suds.client.Client.
+(+)         * Using Transport.
+(+)             * 'nosend' option avoids transport usage.
+(+)             * Sending data via & using data returned by transport.
+(+)                 * Opening a WSDL already tested.
+(+)                 * Sending a web service operation invocation request.
+(+)             * Handling transport errors.
+(+)                 * This seems like a complex topic. Prepare a rough test and
+(+)                   add a todo item to deal with this in more detail later on.
+(+)                     * Embed a TODO comment next to the new test.
+
+(23.02.2014.)
+
+(+) * (Jurko) Refactor suds.transport.http module unit tests to make them
+(+)   clearer.
+
+(27.02.2014.)
+
+(+) * (Jurko) Fix the raw try:/except: exception handling suds which may eat up
+(+)   internal exceptions like SystemExit or KeyboardInterrupt but should not.
+(+)     * Just replace with Exception subclass catching. A more detailed
+(+)       exception class can be used later on if needed.
+
+(28.02.2014.)
+
+(+) * (Jurko) Fix Exception message used when attempting to construct a
+(+)   suds.sax.element.Element with a non-Element parent. It seems like someone
+(+)   forgot to apply % formatting there.
+(+) * (Jurko) Extract existing XML comparison test utilities into a separate
+(+)   tests/test_utility.py module (CompareSAX class).
+
+(03.03.2014.)
+
+(+) * (Jurko) CompareSAX test utility improvements.
+(+)     * Use pytest assertions to report errors.
+(+)     * Add tests.
+(+)     * Extract tests into a separate module.
+(+) * (Jurko) Fix the problem with different processes using different cache ids
+(+)   for matching documents, causing them not to be able to reuse each other's
+(+)   cache entries. Original suds implementation used the built-in hash
+(+)   function which should be replaced with a md5 hash.
+(+)     * On Python 3.3 and above hash function results are seeded with a random
+(+)       value for each process.
+(+)     * 32-bit & 64-bit Python implementations use different hash
+(+)       implementations giving different results.
+(+)     * Update readme.
+(+)         * Problem reported by Eugene Yakubovich at bitbucket.
+(+)     * See project related comments at: 'http://bitbucket.org/eyakubovich/
+(+)       suds/commits/fec6efd9c10b48114bbd9c06847a264453a51620'.
+
+(04.03.2014.)
+
+(+) * (Jurko) suds.sax.enc module cleanup.
+(+)     * Code & comment cleanup.
+(+)     * Add unit tests.
+(+) * (Jurko) Look into CDATA related encoding issues reported at BitBucket
+(+)   (issue #14 + pull requests #25 & #26).
+(+)     * Prepare tests.
+(+)     * Prepare profiling scripts for comparing different solutions.
+(+)         * Design input types.
+(+)             * Long input.
+(+)                 * Replacements.
+(+)                     * None.
+(+)                     * Rare.
+(+)                     * Lots.
+(+)                 * CDATA.
+(+)                     * None.
+(+)                     * Short.
+(+)                     * Long.
+(+)             * Short input.
+(+)                 * Has replacements.
+(+)                     * False.
+(+)                     * True.
+(+)                 * Has CDATA
+(+)                     * False.
+(+)                     * True.
+
+(05.03.2014.)
+
+(+) * (Jurko) Consider reimplementing suds.sax.enc.Encoder.decode() using
+(+)   xml.sax.saxutils.unescape() as suggested by Stephen Fuhry (fuhrysteve at
+(+)   BitBucket).
+(+)     * Profile using the prepared tests.profiling.profile_sax_encoder module.
+(+)         * Python 2.4.3.
+(+)             * About the same for data containing many replacements.
+(+)             * New solution much slower for data containing no replacements.
+(+)         * Python 3.3.3.
+(+)             * New solution a bit slower for data containing many
+(+)               replacements.
+(+)             * New solution much slower for data containing no replacements.
+(+)     * Discard the new solution as it works the same or worse than the
+(+)       current one.
+
+(20.03.2014.)
+
+(+) * (Jurko) Internal project development cleanup.
+(+)     * Setting up Python installations used for testing suds.
+(+)         * Research exact steps for installing required Python packages for
+(+)           each specific Python version used.
+(+)         * Prepare Windows batch script performing this operation en-masse.
+
+(23.03.2014.)
+
+(+) * (Jurko) Report 'except:' issues in setuptools project's ez_setup.py
+(+)   script.
+(+) * (Jurko) Update the project setup procedure to use any preexisting
+(+)   setuptools installation of version >= 1.4 but install the latest
+(+)   compatible setuptools version if a suitable one is not already installed.
+(+)     * YAGNI - using an older and potentially untested setuptools version
+(+)       risks our project installation failing without the user 'doing
+(+)       anything wrong', and gains us only a bit lighter installation in some
+(+)       scenarios.
+(+) * (Jurko) Update the project setup procedure to use setuptools 1.4.2 for
+(+)   Python versions prior to 2.6, and the latest setuptools version 3.3 for
+(+)   all newer Python versions.
+(+)     * Update the project's setup.py script.
+(+)         * Use setuptools 1.4.2 with Python < 2.6.
+(+)         * Use setuptools 3.3 with Python >= 2.6.
+(+)     * Update project hacking documentation.
+(+)         * Check for newer setuptools versions with every new suds release
+(+)           and update the project's setup.py script as needed.
+(+)     * Update release notes.
+
+(15.05.2014.)
+
+(+) * (Jurko) Process work done 'as time allowed' during the last two months.
+(+)     * Commit prepared basic environment setup scripts.
+(+)     * Mark related todo items now completed or rendered obsolete.
+(+) * (Jurko) See if we can make the project setup.py script work when run from
+(+)   a folder other than the project's root folder.
+(+)     * Managed to improve the support a bit, but distutils still has issues
+(+)       regarding this. Should be checked again after a while to see if the
+(+)       situation has changed.
+(+) * (Jurko) Internal project development cleanup.
+(+)     * Setting up Python installations used for testing suds.
+(+)         * Prepare a script for installing required Python packages.
+(+)             * Convert prepared Windows batch script to Python.
+(+)                 * Required Python packages.
+(+)                     * setuptools.
+(+)                     * pip.
+(+)                     * pytest.
+(+)             * Update project hacking documentation.
+(+)             * Allow setting up only specific Python versions.
+(+)                 * Can be configured using setup.cfg for now.
+(+)             * Update to only install packages that have not already been
+(+)               installed.
+(+)                 * Transformed to embedded TODO comments.
+(+)             * Make sure the prepared Python script runs using any of the
+(+)               suds project supported Python versions.
+(+)             * Collect all downloaded data under a single folder.
+(+)                 * Targeted content.
+(+)                     * ez_setup.py downloaded data.
+(+)                     * easy_install downloaded data.
+(+)                     * pip downloaded data.
+(+)             * See if using the pip download cache folder is useful at all
+(+)               since we already pre-download all the required installation
+(+)               packages and use them from there.
+(+)     * Prepare a test script installing suds into multiple Python versions.
+(+)         * Without setuptools preinstalled.
+(+)         * With setuptools preinstalled.
+(+)             * setuptools 1.4.
+(+)             * Latest compatible setuptools release.
+(+)                 * setuptools 1.4.2 for Python releases prior to 2.6.
+(+) * (Jurko) Plan preparing a wheel based distribution.
+(+)     * Test it.
+(+)     * Document.
+(+)         * Setting up the necessary development environment.
+(+)         * Release notes.
+(+) * (Jurko) Find a cleaner way to install suds tests. Currently they get
+(+)   installed into a top-level 'tests' folder and so may cause conflicts with
+(+)   some other 'tests' package that might exist in the target Python
+(+)   environment.
+(+)     * Ideas to consider.
+(+)         * We want all suds users to have easy access to the suds test suite.
+(+)             * Suds users are programmers themselves, and so we want to make
+(+)               it easy for them to reproduce their issues as easily,
+(+)               concisely and directly as possible, and using existing suds
+(+)               tests seems like a perfect starting point for doing just that.
+(+)         * Placing the 'tests' folder under the 'suds' folder.
+(+)         * Placing the top level suds folder outside the egg folder (similar
+(+)           to how this is done for the pytest package), not having the egg
+(+)           folder added to the Python path and leaving the tests folder
+(+)           inside the egg folder.
+(+)         * Have setup.py install suds tests as a setuptools 'extra' - only if
+(+)           explicitly requested.
+(+)             * At first glance, this does not seem like something we would
+(+)               like done, as we want all suds users to have easy access to
+(+)               the suds test suite and not just those installing suds from
+(+)               its source distribution and providing some magic
+(+)               '--install-tests'-like command-line option.
+(+)         * Keep the tests in the source distribution and do not install them.
+(+)             This seems like the simplest way, so lets go with this for now.
+
+(16.05.2014.)
+
+(+) * (Jurko) Find a cleaner way to install suds tests. Currently they get
+(+)   installed into a top-level 'tests' folder and so may cause conflicts with
+(+)   some other 'tests' package that might exist in the target Python
+(+)   environment.
+(+)     * Update the tests so we no longer need to transform them using py2to3
+(+)       in order to run them using Python 3.
+(+)         * Try using the six compatibility package.
+(+)             * Manual install.
+(+)             * Update setup.py.
+(+)             * Update tools/setup_base_environments.py.
+(+)         * Update tests.
+(+)     * Make the tests run using the 'currently installed' suds version. In
+(+)       order to test the current development version, user should install the
+(+)       project in development/editable mode using 'pip install -e' or
+(+)       'setup.py develop'.
+(+)     * Running 'setup.py test' should still work using both Python 2 & 3.
+(+)     * Make project tests run from the root project folder use the correct
+(+)       installed Python 3 sources if run under Python 3 instead of using the
+(+)       ones from the current folder.
+(+)         * No easy way to do this for now. Should look into it again at some
+(+)           later time.
+(+)     * Update project release docs.
+(+)     * Update project HACKING.rst docs.
+(+)         * Using the 'six' Python 2/3 compatibility package.
+(+)         * Running project tests using Python 3 - current working folder.
+
+(22.05.2014.)
+
+(+) * (Jurko) Test using Python 3.4.1.
+
+(26.05.2014.)
+
+(+) * (Jurko) Remove code duplication between setup.py &
+(+)   tools/setup_base_environments.py, e.g. with regards to setting up the
+(+)   project's testing requirement packages (argparse, colorama, py, pytest).
+
+(29.05.2014.)
+
+(+) * (Jurko) Internal project development cleanup.
+(+)     * Re-implement the existing run_all_tests.cmd Windows batch script for
+(+)       running a full suds test suite on multiple Python versions in Python.
+(+)         * Read the list of target Python environments from the project's
+(+)           setup.cfg configuration file.
+(+)         * Make sure the prepared Python script runs using any of the suds
+(+)           project supported Python versions.
+(+)         * Add support for disabling testing a specific Python platform.
+(+)             * YAGNI for now. Can be done by commenting out appropriate
+(+)               environment definition sections in the main project 'setup.py'
+(+)               configuration file and will be affected later on by porting
+(+)               our testing system over to use the tox project.
+(+)         * Update HACKING.rst documentation.
+
+(14.06.2014.)
+
+(+) * (Jurko) Add basic suds.sax.element unit tests.
+
+(15.06.2014.)
+
+(+) * (Jurko) Add tests for suds.sax.element.Element string conversion.
+(+) * (Jurko) Add tests for suds.sax.document.Document string conversion.
+(+) * (Jurko) Fix suds.sax.document.Document str conversion bug.
+(+) * (Jurko) Fix original suds project bug with suds.cache.DocumentCache not
+(+)   caching suds.sax.document.Document instances correctly, even though such
+(+)   instances get passed to it by the suds.reader.DocumentReader.
+(+) * (Jurko) Fix problems with suds silently hiding plugin exceptions.
+(+)     * Add plugin unit testing module
+(+)     * Test that calling a plugin using PluginContainer passes any
+(+)       exceptions raised by the plugin.
+(+)     * Fix the PluginContained exception passing issue.
+(+)     * Close issue #42 at BitBucket.
+(+)     * Close pull request #33 at BitBucket.
+(+) * (Jurko) Fix problem with decimal zero representations having a negative
+(+)   exponent, e.g. as it "0.0000".
+(+)     * Add tests.
+(+)     * Fix failing assert.
+(+)     * Close pull request #37.
+(+)     * Close issue #31.
+
+(23.06.2014.)
+
+(+) * (Jurko) Look at issue #49 at BitBucket - generated SOAP request containing
+(+)   a tag with a missing namespaces identifier.
+(+)     * Research.
+(+)         * Caused by incorrect XSD schema element form attribute handling
+(+)           when the element is actually just a reference to another top-level
+(+)           element. In such cases the the referenced and not the referencing
+(+)           element's form attribute value should be used.
+(+)     * Prepare a quick-fix plugin.
+(+)     * Report back to the BitBucket issue tracker.
+
+(24.06.2014.)
+
+(+) * (Jurko) SchemaObject.form_qualified cleanup.
+(+)     * Add tests for the schema element's form attribute handling.
+(+)         * Element with form attribute set to 'qualified'.
+(+)             * Schema with elementFormDefault set to 'qualified'.
+(+)             * Schema with elementFormDefault set to other than 'qualified'.
+(+)             * Schema with elementFormDefault unset.
+(+)         * Element with form attribute set to other than 'qualified'.
+(+)             * Schema with elementFormDefault set to 'qualified'.
+(+)             * Schema with elementFormDefault set to other than 'qualified'.
+(+)             * Schema with elementFormDefault unset.
+(+)         * Element with unset form attribute.
+(+)             * Schema with elementFormDefault set to 'qualified'.
+(+)             * Schema with elementFormDefault set to other than 'qualified'.
+(+)             * Schema with elementFormDefault unset.
+
+(26.06.2014.)
+
+(+) * (Jurko) Remove unnecessary 'history' list copying in
+(+)   suds.xsd.sxbase.SchemaObject.content().
+(+) * (Jurko) suds.xsd.deplist module cleanup.
+(+)     * Stylistic code & comment cleanup.
+(+)         * PEP8-ify.
+(+)         * Make pop() not ignore exceptions.
+(+)         * Make add() no longer return self.
+(+)         * Remove sorted member.
+(+)         * Order methods alphabetically.
+(+)         * Mark private operations & data as private.
+(+)     * Review the topological sort implementation.
+(+)         * `popped` list seems to be filled only for its elements to later be
+(+)           moved over to the `sorted` list.
+(+)     * Add topological sorting tests by extracting the existing embedded test
+(+)       into a stand-alone unit test module.
+(+) * (Jurko) Look at BitBucket issue #50 - SOAP 1.2 compatibility.
+(+) * (Jurko) Look at BitBucket issue #51 - complexType 'mixed' attribute.
+
+(27.06.2014.)
+
+(+) * (Jurko) suds.xsd.deplist module cleanup.
+(+)     * Reference cycle handling.
+(+)         * Research.
+(+)         * Document.
+(+)         * Test.
+(+)     * Review the topological sort implementation.
+(+)         * See if there is anything obvious to be made more efficient in it.
+(+)         * Simplify code by using a recursive implementation.
+(+)     * Refactor the DepList class into a single function dependency_sort()
+(+)       function taking a single dependency dictionary as input.
+(+)         * Refactor code.
+(+)         * Rename module to suds.xsd.depsort.
+(+)     * Update release notes.
+(+)         * DepList class replaced with a simple dependency_sort() function
+(+)           taking a single dependency dictionary as input.
+(+)             * The original implementation's interface was too heavy-weight
+(+)               with no added value.
+(+)             * Anything tried with the original interface outside the basic
+(+)               use-case covered by dependency_sort() was actually or could be
+(+)               easily broken.
+(+)         * suds.xsd.deplist module renamed to suds.xsd.depsort.
+(+) * (Jurko) SchemaObject.form_qualified cleanup.
+(+)     * Add tests for the schema element's form attribute handling.
+(+)         * Reference elements.
+(+)             * Unqualified referencing qualified.
+(+)                 * Referencing & referenced elements should have different
+(+)                   namespaces.
+(+)             * Qualified referencing unqualified.
+
+(28.06.2014.)
+
+(+) * (Jurko) SchemaObject.form_qualified cleanup.
+(+)     * Fix detected issues.
+(+)         * Fix failing tests.
+(+)         * Mark tests as no longer expected to fail.
+(+)     * Update project release notes.
+(+)     * Report back to issue #49 on BitBucket.
+
+(29.06.2014.)
+
+(+) * (Jurko) SchemaObject.form_qualified cleanup followup.
+(+)     * Research the XSD specification on the element form attribute.
+(+)     * Correct suds's XSD element form attribute handling.
+(+)         * Reference elements are always qualified.
+(+)         * Top-level elements are always qualified.
+(+)         * Non-top-level non-reference elements are qualified based on their
+(+)           form attribute, and if they have no form attribute, their schema's
+(+)           elementFormDefault attribute is used instead, and if that one is
+(+)           not given either - they are considered unqualified.
+
+(30.06.2014.)
+
+    * (Jurko) SchemaObject.form_qualified cleanup followup.
+        * Correct suds's XSD element form attribute handling.
+            * Qualified XSD elements are connected to a namespace.
+            * Unqualified XSD elements are not connected to any namespace.
+            * XML elements matching an unqualified XSD schema element do not
+              necessarily need to have no namespace specified. If their parent
+              element's default namespace is specified, then they still need to
+              explicitly specify their default namespace as an empty string.
+            * Test SOAP XML construction for qualified/unqualified elements with
+              and without the `prefix` suds option enabled.
+        * Document breaking changes between this fork and the original suds
+          project.
+            * DocumentStore interface changes.
+            * Choice support.
+        * SchemaObject.form_qualified should not have 'form_qualified'. That
+          seems like something related only to elements & attributes.
+        * SchemaObject.dependency() method seems too complicated - its interface
+          allows returning a list of dependencies, with a specific one amongst
+          them singled out as a dependency to be merged, when in fact it always
+          returns no dependencies or only a single dependency with that single
+          dependency to be merged.
+            * Rename to dereference() and have it return the referenced
+              SchemaObject - since that is in fact what this function already
+              does.
+        * SchemaObject.merge() is misnamed - it should be named
+          collect_referenced_data() since it is a completely different operation
+          from Schema.merge() and it actually only collects data from a
+          referenced object, if such an object exists.
+        * Look at issue #46 at BitBucket - form attribute on schema attributes
+          does not seem to be handled correctly.
+            * Schema 'attributeFormDefault' attribute handling.
+            * Schema attribute 'form' attribute handling.
+
+    * (Jurko) Look at the Spyne project and its suds usage.
+        * Try running Spyne's test suite using the suds-jurko fork.
+            * Related links.
+                * 'https://spyne.ci.cloudbees.com/job/spyne/PYFLAV=2.7/
+                  lastCompletedBuild/testReport/spyne.test.interop.test_suds/
+                  TestSuds/'.
+                * 'https://github.com/arskom/spyne/blob/master/spyne/test/
+                  interop/test_suds.py'.
+            * Notes from the Spyne maintainer Burak Arslan.
+                * You can run Spyne tests against your suds locally. Just clone
+                  the Spyne repo and edit setup.py to use suds-jurko instead of
+                  suds here:
+                  'https://github.com/arskom/spyne/blob/master/setup.py#L344'.
+            * Send feedback back to the Spyne maintainer (burak.arslan at
+              arskom.com.tr).
+        * Try updating Spyne project test system so it can be easily configured
+          to use suds-jurko instead of the official suds release.
+
+    * (Jurko) Try out virtualenv to make it easier to run tests using multiple
+      Python versions and to allow us to automate suds installation testing in
+      the future.
+        * Compatible virtualenv versions.
+            * virtualenv - 1.7.2 (last before 1.8) - last on Python 2.4.
+            * virtualenv - 1.9.1 (last before 1.10) - last on Python 2.5.
+
+    * (Jurko) Internal project development cleanup.
+        * Prepare a full suds test suite subset that can be used for regular
+          development - select only a few platforms to test suds on regularly
+          and run the full test suite only 'when needed', e.g. before pushing
+          changes onto BitBucket or at the end of a single development session.
+
+    * (Jurko) Add an XML comparison test utility supporting raw XML data
+      comparison with the following features.
+        * Important comparison features.
+            * How textual data is split between child elements.
+            * Child element ordering.
+            * Element/attribute namespace + name.
+            * Non-namespace declaration related attribute ordering.
+        * Unimportant comparison features.
+            * Leading/trailing textual data whitespace per line for mixed
+              content nodes containing at least one child element.
+            * Element/attribute namespace prefix.
+            * Namespace declaration related attribute ordering.
+            * Namespace declarations (no prefix).
+            * Namespace prefix declarations.
+
+    * (Jurko) Add tests for the functionality potentially affected by a planned
+      _SoapClient/_SimClient refactoring.
+        * _SoapClient - currently can only be tested via suds.client.Client.
+            * 'prettyxml' option.
+            * 'retxml' option.
+        * _SimClient - currently can only be tested via suds.client.Client.
+            * Using Transport for sending an injected request.
+            * Not using Transport when a reply has been injected.
+            * _SimClient using injected request.
+            * _SimClient using injected reply.
+                * Success.
+                * Failure.
+                * Status & status description.
+            * 'nosend' option.
+                * With injected request data.
+                * With injected response data - ignored.
+            * 'retxml' option.
+            * 'prettyxml' option.
+        * RequestContext.
+            * Returned from _SoapClient.
+            * Returned from _SimClient.
+            * Processing an externally provided reply.
+        * Plugin processing.
+            * Variants.
+                * _SoapClient/_SimClient.
+                * _SimClient with injected reply/response.
+                * 'nosend' option + RequestContext reply.
+                * 'retxml'.
+                * 'prettyxml'.
+            * Fetched documents.
+                * document.loaded(url, bytes) --> bytes.
+                * document.parsed(url, document).
+            * Documents read from cache.
+                * document.parsed(url, document).
+            * Client initialized with WSDL.
+                * init.initialized(wsdl).
+            * SOAP requests.
+                * marshalled(document) - may modify document in-place.
+                * sending(bytes) --> bytes.
+            * SOAP replies.
+                * received(bytes) --> bytes.
+                * parsed(element) --> may modify document in-place.
+                * unmarshalled(result) --> result.
+
+    * (Jurko) Refactor _SoapClient/_SimClient related code.
+        * Refactor.
+            * Already prepared as client_1.py on Jurko's notebook but need to
+              add relevant tests first.
+        * Test.
+            * _SoapClient subclasses modifying _init_invocation_context(),
+              _get_request() & _get_reply().
         * Update release notes.
+            * Client debug log messages changed.
+            * Removed the RequestContext.original_envelope attribute.
+
+    * (Jurko) See why creating two suds.client.Client instances with the same
+      transport instance fails. It reports some 'Duplicate domain "suds.options"
+      found' error.
+        t = suds.transport.Transport()
+        tests.client_from_wsdl(tests.wsdl(""), transport=t)
+        tests.client_from_wsdl(tests.wsdl(""), transport=t)
+
+    * (Jurko) Generic code cleanup.
+        * HttpTransport tests.
+            * Check for multiple build_opener() calls.
+            * Check for multiple urlopener.open() calls.
+            * Check what gets passed to the default built urlopener. Merge this
+              with externally specified urlopener testing.
+        * Add HttpTransport proxy configuration tests.
+            * See when the proxy member is set.
+            * See what the default proxy member value is for.
+            * See whether the proxy member is used at all with an externally
+              specified urlopener, and if so - if it is made redundant by using
+              such externally specified urlopeners.
+        * Add cachingpolicy option tests.
+        * Add default document store tests.
+            * Generic.
+            * suds.client.Client document store usage.
+        * See why exception raised when passing a transport to
+          suds.client.Client that is not a subclass of suds.transport.Transport
+          is formatted as it is.
+            * Class's string representation wrapped in a tuple.
+            * Uses class's repr() string representation.
+            * Double quotes used around the 'transport'.
+        * suds.store.defaultDocumentStore cleanup.
+            * See if this member should be marked as private.
+            * Rename to not use camelCase.
+        * DefinitionsReader.open() changes options on cached Definitions
+          instances. Make sure this does not break programs with multiple suds
+          Clients loading the same WSDL schema but using different options.
+        * Support for calling NoCache.purge() & NoCache.clear().
+        * Consider making FileCache.clear() remove files matching both prefix &
+          suffix instead of only the prefix.
+            * Might be ok for explicitly called clears, but not for those called
+              due to the cache version information getting changed. This might
+              need to be worked around or commented/tested in code.
+        * Make FileCache, DocumentCache & ObjectCache all consistently remove or
+          not remove cache entries on failed access. Possibly do so for the base
+          Cache class as well.
+        * Make Cache.put() not return the cached data.
+        * FileCache.put() operations should not leave behind invalid files in
+          case of failed write operations. Possibly first store a valid file
+          under a temporary name and then rename it.
+
+    * (Jurko) Restore last_sent()/last_received() getters for last used SOAP
+      request/response messages.
+        * Research.
+            * Difference between how _SoapClient & _SimClient stored their last
+              sent/received messages.
+                * Both stored them.
+                * _SimClient stored the simulated fault as its last received
+                  message.
+            * Whether sent/received messages should be returned/logged before or
+              after being processed by registered plugins.
+                * _SoapClient stored the received reply after it got processed
+                  by registered plugins.
+            * Sending with _SoapClient.
+            * Sending with _SimClient.
+            * Which plugins get called and when.
+        * Add back the removed code.
+        * Add tests.
+        * Notify people interested in this.
 
     * (Jurko) Clean up input argument/parameter & wrapped/unwrapped terminology
       usage in code.
         * Synchronize all usage both in code and in relevant 'notes/' documents.
 
+    * (Jurko) Triage received issues & pull requests at BitBucket a bit and
+      prioritize them in relation to already existing todo items on this list.
+
     * (Jurko) Process Jurko's remaining research & todo notes collected while
       working on reporting extra parameter errors. Some are redundant or have
       already been dealt with so the list needs to be triaged first.
@@ -817,13 +1712,13 @@ PRIORETIZED:
         * Add test: single parameter pointing to a built-in type - no
           unwrapping.
         * Add test: multiple parameters pointing to types & elements.
-        * Add tests: wsdl tests - XSD schema content.
-        * Add tests: wsdl tests - recognized operations.
-        * Add tests: wsdl tests - recognized services.
+        * Add tests: WSDL tests - XSD schema content.
+        * Add tests: WSDL tests - recognized operations.
+        * Add tests: WSDL tests - recognized services.
         * Add test: document/literal, document/encoded, rpc/literal &
           rpc/encoded should make no difference to input processing in theory
           and should all recognize the same input parameter structure.
-        * Add tests: wsdl tests - recognized binding - input/output - for
+        * Add tests: WSDL tests - recognized binding - input/output - for
           different methods - document binding should be the default - see
           whether literal should be the default binding style.
         * Add test: named sequence/choice/all - there is no such thing - their
@@ -938,25 +1833,111 @@ PRIORETIZED:
               parameters) should not be binding class specific, i.e. it should
               be shared for both document & rpc bindings.
 
-    * (Jurko) Clean up the project's basic development script for running the
-      full suds test suite using multiple Python interpreter versions.
-        * Reimplement in Python.
-        * Name better.
-
-    * (Jurko) Find a cleaner way to install suds tests. Currently they get
-      installed into a top-level 'tests' folder and so may cause conflicts with
-      some other 'tests' package that might exist in the target Python
-      environment.
-        * Ideas to consider.
-            * Placing the 'tests' folder under the 'suds' folder.
-            * Placing the top level suds folder outside the egg folder (similar
-              to how this is done for the pytest package), not having the egg
-              folder added to the Python path and leaving the tests folder
-              inside the egg folder.
-
-NON PRIORETIZED:
+    * Code cleanup
+        * It seems suds ignores top-level element names & namespaces when
+          parsing server responses and instead just uses them by their index.
+          This seems wrong. Check the WSDL specification, SOAP message binding
+          and other related standards to see if the server must always return
+          its values in the correct order in which the corresponding output
+          message parts are defined in the WSDL. Plan updating suds to report
+          invalid server responses.
+        * Replace except: blocks with blocks catching specific exception types.
+        * suds.xsd.schema module imports suds.xsd.sxbuiltin.Factory as
+          BuiltinFactory but never used that reference. See if this interface is
+          perhaps documented as published and if not - remove it.
+        * Performance tuning - SchemaObject.str() should not use len(self) to
+          check whether it is empty.
+        * SchemaObject.str() - see whether any contained item information is
+          included if none of the items are instances of Content.
+        * TypedContent.qref() - see whether len(self) == 0 check for 'simple
+          types' can be improved. There should be no need to calculate the whole
+          length just to determine that the object is empty.
+        * Simple.mixed(), SimpleContent.mixed()  - see if they really need to
+          return the current object length or if they can just check whether the
+          object is empty.
+        * suds.sudsobject.footprint() - see if the len() check is really needed
+          just to see if the object is empty.
+            * Check other len() usage in suds.sudsobject.
+        * Check all 'except:' blocks to see if they actually need to be such
+          catch-all blocks.
+
+    * (Jurko) Look into issues #7 & #13 on BitBucket - problems loading a WSDL
+      containing a recursively defined XML schema.
+
+NON PRIORITIZED:
 =================================================
 
+    * suds.sudsobject.footprint() does some fancy recursive counting while all
+      it is actually used for is for detecting objects with footprint 0. This
+      could be optimized by replacing it with a function returning simply
+      whether a given object has a non-empty footprint and returning as soon as
+      it find something of value in it.
+
+    * Add suds.sax.element related tests.
+        * append().
+            * Add child element.
+            * Add child attribute.
+        * childAtPath().
+            * With namespaces.
+            * With prefixes.
+            * Empty child name - as initial child - requested.
+            * Empty child name - as initial child - pass through.
+            * Empty child name - as indirect child - requested.
+            * Empty child name - as indirect child - pass through.
+            * Handling multiple children with matching names - is there a way to
+              specify an index.
+        * Add child by constructing it with a parent argument - should
+          automatically add the element to the parent's children list, while the
+          current implementation only makes the child know about the parent and
+          does not let the parent know about the child.
+        * Constructing an element with '/' in its name should not be allowed
+          since that character is used as a part separator.
+            * See if a way to escape such character usage is needed and if so -
+              how to do it.
+        * Handling multiple children with matching names.
+        * childrenAtPath().
+        * Initializing a new element with a specific namespace.
+
+    * Add suds.sax.document unit tests similar to those already added for
+      suds.sax.element.
+        * Some ideas on what to not forget.
+            * See suds.sax.element related tests & todo notes.
+            * childAtPath().
+            * childrenAtPath().
+            * No root element.
+
+    * Look into CDATA related encoding issues reported at BitBucket (issue #14 +
+      pull requests #25 & #26).
+        * Current status: waiting for further feedback or progress on BitBucket.
+        * Implement a working solution satisfying the prepared tests.
+            * Use the prepared profiling script to make sure this change does
+              not degrade general suds performance.
+        * Get more feedback on a seemingly bad 'embedded CDATA section' test
+          included in the pull request #26 on BitBucket.
+            * Support for nested CDATA sections, in violation of the CDATA
+              related documentation found at
+              'http://en.wikipedia.org/wiki/CDATA#Nesting' and
+              'http://www.w3.org/TR/REC-xml/#sec-cdata-sect'.
+        * Decide how unclosed CDATA sections should be handled.
+            * Suggestion: raise an exception since this is not allowed in well
+              formed XML documents.
+
+    * Resolving XSD schema elements currently returns an XSD schema type object
+      representing the element's type. Different elements with matching types
+      should possibly resolve to the same XSD schema type object but currently
+      do not. See if this behaviour should be corrected.
+        * Example.
+            * XSD schema:
+                <xsd:element name="wi" type="xsd:string"/>
+                <xsd:element name="wo" type="xsd:string"/>
+            * Test code:
+                schema = client.wsdl.schema
+                element_in = schema.elements["wi", "my-namespace"]
+                element_out = schema.elements["wo", "my-namespace"]
+                assert element_in.resolve() is element_out.resolve()
+            * Both elements should possibly resolve to the same XString
+              instance, but currently they resolve to two separate ones.
+
     * Optionally make suds check that the input parameters passed to its web
       service proxy operations actually match their respective WSDL definitions.
         * Current suds behaviour.
@@ -996,7 +1977,7 @@ NON PRIORETIZED:
           'xxxtest_choice_parameter_implementation_inconsistencies' unit test
           demonstrating a problem with the current implementation.
 
-    * SoapClient 'location' cleanup.
+    * _SoapClient 'location' cleanup.
         * URL quoting, especially if specified externally by the caller instead
           of having been read from a valid WSDL schema.
 
@@ -1006,7 +1987,7 @@ NON PRIORETIZED:
         * See how this sort of data is supposed to be entered anyway.
 
     * FileCache class fixes.
-        * Remove incorrectly created cache files, e.g. if it gets created but
+        * Remove incorrectly created cache files, e.g. if one gets created but
           then writing to it fails.
         * Make sure reading a cache file does not crash the whole Python
           interpreter process.
@@ -1028,12 +2009,11 @@ NON PRIORETIZED:
             * May be run without additional pytest options by running 'setup.py
               test' from the root project folder.
                 * This method will automatically download & install pytest from
-                  PyPI if needed, but will leave them installed in the project's
-                  root folder.
-            * May be run from the root project folder by first building the
-              project to a temporary location: 'setup.py build & pytest build'.
-                * This method allows specifying additional pytest command-line
-                  arguments.
+                  PyPI if needed, but will leave their installations behind in
+                  the project's root folder.
+            * May be run by first building the project by installing the
+              project, e.g. by running 'setup.py develop' and then running
+              'pytest' from the project's 'tests' folder.
         * Do some more thinking on this and, when done, update related HACKING
           notes.
 
@@ -1078,7 +2058,7 @@ NON PRIORETIZED:
         * Implement.
 
     * Prepare additional test cases.
-        * Loading a wsdl from a file as in suds.client.Client(
+        * Loading a WSDL from a file as in suds.client.Client(
           "file://localhost/Folder/aaa.wsdl").
             * Path not containing spaces.
             * Path containing spaces.
@@ -1115,3 +2095,177 @@ NON PRIORETIZED:
         * For example, 'integer' or 'string' elements defined in the
           'http://schemas.xmlsoap.org/soap/encoding/' namespace (stored
           internally by suds in the store.py module).
+
+    * See whether importing an external WSDL schema can benefit from that
+      schema's WSDL object already being cached.
+        * See if this has already been implemented, and if not - implement it.
+
+    * Additional test ideas.
+        * External XSD usage (including both imported and included external XSD
+          schemas).
+            * All the different external XSD usage tests should be run in
+              scenarios when the external XSD gets used from an imported WSDL.
+            * Using an external XSD schema using another external XSD schema.
+        * Test pluggable sax character reference encoder support.
+            * Have tests plug in a custom character reference encoder and try it
+              out.
+
+    * A HttpTransport.send() operation will return None if the web service
+      server responds with HTTP status code ACCEPTED (202) or NO_CONTENT (204),
+      but the _SoapClient method using this operation is not prepared to handle
+      this and will attempt to access the 'message' member on the returned
+      value.
+        * Failing tests have already been added for this.
+        * Alternative ideas on how to fix this.
+            * Continue returning None in these cases and fix up the calling
+              code.
+            * Raise a separate exception.
+            * Wrap this information in a regular suds.transport.Reply instance.
+            * Possibly handle this differently based based on whether the web
+              service operation invoked has any defined output message parts.
+
+    * 'soapheaders' suds option usage cleanup.
+        * Add tests.
+            * SOAP headers given as a list/tuple.
+                * Strings.
+                * Elements.
+                    * Initial.
+                    * Middle.
+                    * Last.
+                    * Multiple.
+            * SOAP headers given as a dictionary.
+                * Strings.
+                * Elements - error.
+            * Unknown header handling.
+                * Too many when given as list/tuple.
+                * Unknown key when given as dictionary.
+            * Plugins modifying SOAP header elements should not affect future
+              requests constructed using those same headers.
+        * Note that Elements found in the soapheaders option need to be copied
+          into each request so any plugins updating that request's content would
+          not update the original SOAP header elements and thus affect all
+          future requests constructed using those same SOAP headers.
+        * Fix bug: Elements added to a soapheaders list before all expected
+          headers have been added, but later ones are ignored.
+        * Raise an exception in case of an unrecognized header.
+            * Unrecognized header name in a dictionary.
+            * Too many non-Element headers in a list/tuple.
+        * Better document the 'soapheaders' suds option.
+        * At first glance it seems suds does not use reply headers the same as
+          it does its request headers. See if SOAP web service HTTP bindings
+          allow output headers at all, and if so, how suds should support this.
+        * See if header elements can be marked as required, and if so, see if
+          suds should report an error if a required header value has not been
+          provided.
+        * See if header elements can be marked as optional and how suds should
+          handle this.
+        * See if header elements can have multiple occurrences, and if so, how
+          suds should support them.
+        * See how header values can be specified more easily for a single method
+          invocation.
+
+    * See if input message parts defined using the 'type' attribute (as opposed
+      to those marked using the 'element' attribute) respect their optional/
+      required status. PartElement class used to model such part's element
+      information seems to always return True from its optional() method.
+        * If this is wrong - add a test demonstrating the problem.
+        * If this is correct - see what the suspicious optional() method is for.
+
+    * Look at suds.sax.element.Element.insert() - its documentation says it
+      accepts either an element or an attribute or a collection of elements or
+      attributes and adds it/them as a child/children under the current element.
+        * From glancing through the code it does not seem to work when passed a
+          collection of elements or attributes.
+            * Most likely it needs a check whether the passed objects parameter
+              is a list or a tuple before it wraps it inside a tuple.
+        * From glancing through the code it does not seems to work when passed
+          an Attribute.
+        * See what to do about this - either update the related function
+          documentation or update it to work as documented.
+        * See how similar functionality has been implemented in
+          suds.sax.element.Element.append().
+
+    * Look at suds.sax.element.Element.childAtPath() and see whether it should
+      return None or self if given an empty string or a string consisting only
+      of slashes.
+        * Currently it returns None in this case as if the given path could not
+          be found.
+        * Returning self or raising an exception seems consistent with the fact
+          that a non-empty path is interpreted relative to self.
+
+    * suds.sax.element.Element.__childrenAtPath() seems smelly. See how it can
+      be simplified and made to work for paths not containing slashes as well as
+      those containing slashes.
+        * This should simplify the suds.sax.element.Element.childrenAtPath()
+          implementation as well.
+
+    * suds.sax parser peculiarities to look into.
+        * Handling mixed content elements, i.e. those that have both child
+          elements and textual content.
+            * All the textual content is concatenated into a single string,
+              independent of how it distributed between child elements in the
+              original parsed XML string.
+                * As a consequence, converting the parsed XML document back into
+                  an XML string will place all the element's textual content
+                  before its child elements.
+            * The textual content has its whitespace trimmed.
+                * Note that this causes the '<a>  42 <b/> <c/> 42  </a>' XML to
+                  be parsed so that <a> element's textual data is: '42   42'
+                  where the whitespace outside the '42' markers is trimmed but
+                  the whitespace between them is preserved.
+
+    * See if we should make sure any plugin exceptions get logged correctly (as
+      was the case when suds.plugin.Method.__call__() was eating up all such
+      exception in the original suds project).
+
+    * The original suds project's DocumentCache.put() implementation accepted
+      suds.sax.element.Element instances as input so we kept this behaviour for
+      now but that might have actually been a result of a bug. We should review
+      this and update related tests & implementation if we no longer wish to
+      support this.
+        * See the TestDocumentCache.test_cache_element() test in
+          'test_sax_element.py'.
+
+    * Improve the XML comparison test utilities.
+        * Add test utility tests.
+        * Make SAX comparison compare XML attributes - order is irrelevant.
+        * Support for comparing two XML data strings in more detail, with
+          explicitly enabling/disabling which differences to consider important.
+            * Some ideas on what might or might not be considered important.
+                * Leading/trailing textual data whitespace.
+                    * Per element.
+                    * Per block between child elements.
+                    * Per input line.
+                    * All whitespace blocks considered equivalent.
+                    * Possibly consider leading/trailing textual data whitespace
+                      unimportant only inside mixed content nodes containing at
+                      least one child element.
+                * How a mixed content node's textual content is split between
+                  its child elements.
+                * Attribute declaration order.
+                * Child element ordering.
+                * Namespace declarations (no prefix).
+                * Namespace prefix declarations.
+                * External references - to parse them or not.
+                * Extra elements on either side.
+                * XML node namespace prefixes.
+                * XML node namespaces.
+                * Input code-page.
+                * Extra data.
+                     * Before XML.
+                     * Surrounding top level XML nodes.
+                * Each feature importance might be enabled/disabled globally or
+                  for a specific XML part only.
+
+    * Add SOAP 1.2 support.
+        * Related links.
+            * Project's issue #50 on the BitBucket issue tracker:
+              https://bitbucket.org/jurko/suds/issue/50#comment-10940316
+            * http://stackoverflow.com/questions/2370573/soap-1-2-python-client
+
+    * Add support for the `mixed` attribute on `complexType` XSD schema
+      elements.
+        * They should allow the use of textual data, possibly in addition to
+          other child elements & attributes.
+        * Related link:
+          https://bitbucket.org/jurko/suds/issue/51#comment-10941116
diff --git a/ez_setup.py b/ez_setup.py
index 72d35a5..10d299b 100644
--- a/ez_setup.py
+++ b/ez_setup.py
@@ -1,14 +1,14 @@
-#!python
+#!/usr/bin/env python
 """Bootstrap setuptools installation
 
-If you want to use setuptools in your package's setup.py, just include this
-file in the same directory with it, and add this to the top of your setup.py::
+To use setuptools in your package's setup.py, include this
+file in the same directory and add this to the top of your setup.py::
 
     from ez_setup import use_setuptools
     use_setuptools()
 
-If you want to require a specific version of setuptools, set a download
-mirror, or use an alternate download directory, you can do so by supplying
+To require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, simply supply
 the appropriate options to ``use_setuptools()``.
 
 This file can also be run as a script to install or upgrade setuptools.
@@ -17,50 +17,38 @@ import os
 import shutil
 import sys
 import tempfile
-import tarfile
+import zipfile
 import optparse
 import subprocess
 import platform
+import textwrap
+import contextlib
 
 from distutils import log
 
 try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+try:
     from site import USER_SITE
 except ImportError:
     USER_SITE = None
 
-DEFAULT_VERSION = "1.4"
+DEFAULT_VERSION = "5.1"
 DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
 
 def _python_cmd(*args):
+    """
+    Return True if the command succeeded.
+    """
     args = (sys.executable,) + args
     return subprocess.call(args) == 0
 
-def _check_call_py24(cmd, *args, **kwargs):
-    res = subprocess.call(cmd, *args, **kwargs)
-    class CalledProcessError(Exception):
-        pass
-    if not res == 0:
-        msg = "Command '%s' return non-zero exit status %d" % (cmd, res)
-        raise CalledProcessError(msg)
-vars(subprocess).setdefault('check_call', _check_call_py24)
-
-def _install(tarball, install_args=()):
-    # extracting the tarball
-    tmpdir = tempfile.mkdtemp()
-    log.warn('Extracting in %s', tmpdir)
-    old_wd = os.getcwd()
-    try:
-        os.chdir(tmpdir)
-        tar = tarfile.open(tarball)
-        _extractall(tar)
-        tar.close()
-
-        # going in the directory
-        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
-        os.chdir(subdir)
-        log.warn('Now working in %s', subdir)
 
+def _install(archive_filename, install_args=()):
+    with archive_context(archive_filename):
         # installing
         log.warn('Installing Setuptools')
         if not _python_cmd('setup.py', 'install', *install_args):
@@ -68,47 +56,68 @@ def _install(tarball, install_args=()):
             log.warn('See the error message above.')
             # exitcode will be 2
             return 2
-    finally:
-        os.chdir(old_wd)
-        shutil.rmtree(tmpdir)
 
 
-def _build_egg(egg, tarball, to_dir):
-    # extracting the tarball
+def _build_egg(egg, archive_filename, to_dir):
+    with archive_context(archive_filename):
+        # building an egg
+        log.warn('Building a Setuptools egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+class ContextualZipFile(zipfile.ZipFile):
+    """
+    Supplement ZipFile class to support context manager for Python 2.6
+    """
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def __new__(cls, *args, **kwargs):
+        """
+        Construct a ZipFile or ContextualZipFile as appropriate
+        """
+        if hasattr(zipfile.ZipFile, '__exit__'):
+            return zipfile.ZipFile(*args, **kwargs)
+        return super(ContextualZipFile, cls).__new__(cls)
+
+
+ at contextlib.contextmanager
+def archive_context(filename):
+    # extracting the archive
     tmpdir = tempfile.mkdtemp()
     log.warn('Extracting in %s', tmpdir)
     old_wd = os.getcwd()
     try:
         os.chdir(tmpdir)
-        tar = tarfile.open(tarball)
-        _extractall(tar)
-        tar.close()
+        with ContextualZipFile(filename) as archive:
+            archive.extractall()
 
         # going in the directory
         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
         os.chdir(subdir)
         log.warn('Now working in %s', subdir)
-
-        # building an egg
-        log.warn('Building a Setuptools egg in %s', to_dir)
-        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+        yield
 
     finally:
         os.chdir(old_wd)
         shutil.rmtree(tmpdir)
-    # returning the result
-    log.warn(egg)
-    if not os.path.exists(egg):
-        raise IOError('Could not build the egg.')
 
 
 def _do_download(version, download_base, to_dir, download_delay):
     egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
                        % (version, sys.version_info[0], sys.version_info[1]))
     if not os.path.exists(egg):
-        tarball = download_setuptools(version, download_base,
+        archive = download_setuptools(version, download_base,
                                       to_dir, download_delay)
-        _build_egg(egg, tarball, to_dir)
+        _build_egg(egg, archive, to_dir)
     sys.path.insert(0, egg)
 
     # Remove previously-imported pkg_resources if present (see
@@ -121,11 +130,10 @@ def _do_download(version, download_base, to_dir, download_delay):
 
 
 def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
-                   to_dir=os.curdir, download_delay=15):
-    # making sure we use the absolute path
+        to_dir=os.curdir, download_delay=15):
     to_dir = os.path.abspath(to_dir)
-    was_imported = 'pkg_resources' in sys.modules or \
-        'setuptools' in sys.modules
+    rep_modules = 'pkg_resources', 'setuptools'
+    imported = set(sys.modules).intersection(rep_modules)
     try:
         import pkg_resources
     except ImportError:
@@ -133,23 +141,24 @@ def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
     try:
         pkg_resources.require("setuptools>=" + version)
         return
-    except pkg_resources.VersionConflict:
-        e = sys.exc_info()[1]
-        if was_imported:
-            sys.stderr.write(
-            "The required version of setuptools (>=%s) is not available,\n"
-            "and can't be installed while this script is running. Please\n"
-            "install a more recent version first, using\n"
-            "'easy_install -U setuptools'."
-            "\n\n(Currently using %r)\n" % (version, e.args[0]))
-            sys.exit(2)
-        else:
-            del pkg_resources, sys.modules['pkg_resources']    # reload ok
-            return _do_download(version, download_base, to_dir,
-                                download_delay)
     except pkg_resources.DistributionNotFound:
-        return _do_download(version, download_base, to_dir,
-                            download_delay)
+        return _do_download(version, download_base, to_dir, download_delay)
+    except pkg_resources.VersionConflict as VC_err:
+        if imported:
+            msg = textwrap.dedent("""
+                The required version of setuptools (>={version}) is not available,
+                and can't be installed while this script is running. Please
+                install a more recent version first, using
+                'easy_install -U setuptools'.
+
+                (Currently using {VC_err.args[0]!r})
+                """).format(VC_err=VC_err, version=version)
+            sys.stderr.write(msg)
+            sys.exit(2)
+
+        # otherwise, reload ok
+        del pkg_resources, sys.modules['pkg_resources']
+        return _do_download(version, download_base, to_dir, download_delay)
 
 def _clean_check(cmd, target):
     """
@@ -169,10 +178,16 @@ def download_file_powershell(url, target):
     trust). Raise an exception if the command cannot complete.
     """
     target = os.path.abspath(target)
+    ps_cmd = (
+        "[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
+        "[System.Net.CredentialCache]::DefaultCredentials; "
+        "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)"
+        % vars()
+    )
     cmd = [
         'powershell',
         '-Command',
-        "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
+        ps_cmd,
     ]
     _clean_check(cmd, target)
 
@@ -180,14 +195,11 @@ def has_powershell():
     if platform.system() != 'Windows':
         return False
     cmd = ['powershell', '-Command', 'echo test']
-    devnull = open(os.path.devnull, 'wb')
-    try:
+    with open(os.path.devnull, 'wb') as devnull:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except Exception:
             return False
-    finally:
-        devnull.close()
     return True
 
 download_file_powershell.viable = has_powershell
@@ -198,14 +210,11 @@ def download_file_curl(url, target):
 
 def has_curl():
     cmd = ['curl', '--version']
-    devnull = open(os.path.devnull, 'wb')
-    try:
+    with open(os.path.devnull, 'wb') as devnull:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except Exception:
             return False
-    finally:
-        devnull.close()
     return True
 
 download_file_curl.viable = has_curl
@@ -216,14 +225,11 @@ def download_file_wget(url, target):
 
 def has_wget():
     cmd = ['wget', '--version']
-    devnull = open(os.path.devnull, 'wb')
-    try:
+    with open(os.path.devnull, 'wb') as devnull:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except Exception:
             return False
-    finally:
-        devnull.close()
     return True
 
 download_file_wget.viable = has_wget
@@ -233,42 +239,33 @@ def download_file_insecure(url, target):
     Use Python to download the file, even though it cannot authenticate the
     connection.
     """
+    src = urlopen(url)
     try:
-        from urllib.request import urlopen
-    except ImportError:
-        from urllib2 import urlopen
-    src = dst = None
-    try:
-        src = urlopen(url)
-        # Read/write all in one block, so we don't create a corrupt file
-        # if the download is interrupted.
+        # Read all the data in one block.
         data = src.read()
-        dst = open(target, "wb")
-        dst.write(data)
     finally:
-        if src:
-            src.close()
-        if dst:
-            dst.close()
+        src.close()
+
+    # Write all the data in one block to avoid creating a partial file.
+    with open(target, "wb") as dst:
+        dst.write(data)
 
 download_file_insecure.viable = lambda: True
 
 def get_best_downloader():
-    downloaders = [
+    downloaders = (
         download_file_powershell,
         download_file_curl,
         download_file_wget,
         download_file_insecure,
-    ]
-
-    for dl in downloaders:
-        if dl.viable():
-            return dl
+    )
+    viable_downloaders = (dl for dl in downloaders if dl.viable())
+    return next(viable_downloaders, None)
 
 def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
-                        to_dir=os.curdir, delay=15,
-                        downloader_factory=get_best_downloader):
-    """Download setuptools from a specified location and return its filename
+        to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader):
+    """
+    Download setuptools from a specified location and return its filename
 
     `version` should be a valid setuptools version number that is available
     as an egg for download under the `download_base` URL (which should end
@@ -281,74 +278,20 @@ def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
     """
     # making sure we use the absolute path
     to_dir = os.path.abspath(to_dir)
-    tgz_name = "setuptools-%s.tar.gz" % version
-    url = download_base + tgz_name
-    saveto = os.path.join(to_dir, tgz_name)
+    zip_name = "setuptools-%s.zip" % version
+    url = download_base + zip_name
+    saveto = os.path.join(to_dir, zip_name)
     if not os.path.exists(saveto):  # Avoid repeated downloads
         log.warn("Downloading %s", url)
         downloader = downloader_factory()
         downloader(url, saveto)
     return os.path.realpath(saveto)
 
-
-def _extractall(self, path=".", members=None):
-    """Extract all members from the archive to the current working
-       directory and set owner, modification time and permissions on
-       directories afterwards. `path' specifies a different directory
-       to extract to. `members' is optional and must be a subset of the
-       list returned by getmembers().
-    """
-    import copy
-    import operator
-    from tarfile import ExtractError
-    directories = []
-
-    if members is None:
-        members = self
-
-    for tarinfo in members:
-        if tarinfo.isdir():
-            # Extract directories with a safe mode.
-            directories.append(tarinfo)
-            tarinfo = copy.copy(tarinfo)
-            tarinfo.mode = 448  # decimal for oct 0700
-        self.extract(tarinfo, path)
-
-    # Reverse sort directories.
-    if sys.version_info < (2, 4):
-        def sorter(dir1, dir2):
-            return cmp(dir1.name, dir2.name)
-        directories.sort(sorter)
-        directories.reverse()
-    else:
-        directories.sort(key=operator.attrgetter('name'), reverse=True)
-
-    # Set correct owner, mtime and filemode on directories.
-    for tarinfo in directories:
-        dirpath = os.path.join(path, tarinfo.name)
-        try:
-            self.chown(tarinfo, dirpath)
-            self.utime(tarinfo, dirpath)
-            self.chmod(tarinfo, dirpath)
-        except ExtractError:
-            e = sys.exc_info()[1]
-            if self.errorlevel > 1:
-                raise
-            else:
-                self._dbg(1, "tarfile: %s" % e)
-
-
 def _build_install_args(options):
     """
     Build the arguments to 'python setup.py install' on the setuptools package
     """
-    install_args = []
-    if options.user_install:
-        if sys.version_info < (2, 6):
-            log.warn("--user requires Python 2.6 or later")
-            raise SystemExit(1)
-        install_args.append('--user')
-    return install_args
+    return ['--user'] if options.user_install else []
 
 def _parse_args():
     """
@@ -367,16 +310,23 @@ def _parse_args():
         const=lambda: download_file_insecure, default=get_best_downloader,
         help='Use internal, non-validating downloader'
     )
+    parser.add_option(
+        '--version', help="Specify which version to download",
+        default=DEFAULT_VERSION,
+    )
     options, args = parser.parse_args()
     # positional arguments are ignored
     return options
 
-def main(version=DEFAULT_VERSION):
+def main():
     """Install or upgrade setuptools and EasyInstall"""
     options = _parse_args()
-    tarball = download_setuptools(download_base=options.download_base,
-        downloader_factory=options.downloader_factory)
-    return _install(tarball, _build_install_args(options))
+    archive = download_setuptools(
+        version=options.version,
+        download_base=options.download_base,
+        downloader_factory=options.downloader_factory,
+    )
+    return _install(archive, _build_install_args(options))
 
 if __name__ == '__main__':
     sys.exit(main())
diff --git a/ez_setup.py b/ez_setup_1_4_2.py
similarity index 97%
copy from ez_setup.py
copy to ez_setup_1_4_2.py
index 72d35a5..ba4c246 100644
--- a/ez_setup.py
+++ b/ez_setup_1_4_2.py
@@ -29,7 +29,7 @@ try:
 except ImportError:
     USER_SITE = None
 
-DEFAULT_VERSION = "1.4"
+DEFAULT_VERSION = "1.4.2"
 DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
 
 def _python_cmd(*args):
@@ -184,7 +184,9 @@ def has_powershell():
     try:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
             return False
     finally:
         devnull.close()
@@ -202,7 +204,9 @@ def has_curl():
     try:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
             return False
     finally:
         devnull.close()
@@ -220,7 +224,9 @@ def has_wget():
     try:
         try:
             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
-        except:
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
             return False
     finally:
         devnull.close()
diff --git a/notes/xsd_elements.rst b/notes/xsd_elements.rst
new file mode 100644
index 0000000..a9a7536
--- /dev/null
+++ b/notes/xsd_elements.rst
@@ -0,0 +1,59 @@
+==========================
+XSD element research notes
+==========================
+:Authors: Jurko Gospodnetiæ
+:Date: 2014-06-29
+
+
+Global/local elements
+---------------------
+
+XSD schema elements are separated into two disjunct groups:
+
+_`global`
+  top-level elements and non-top-level reference elements (such references may
+  only reference top-level elements, whether from the same or another schema)
+
+_`local`
+  non-top-level non-reference elements
+
+
+_`Qualified`/_`unqualified` elements
+------------------------------------
+
+An XSD element is considered qualified if and only if one of the following
+holds:
+
+* it is `global`_
+* its ``form`` attribute is set to ``qualified``
+* its ``form`` attribute is unset and its schema's ``elementFormDefault``
+  attribute is set to ``qualified``
+
+**suds implementation note:** The only allowed ``form`` & ``elementFormDefault``
+attribute values are ``qualified`` & ``unqualified`` but ``suds`` interprets any
+value other than ``qualified`` as ``unqualified``.
+
+
+Element's _`target namespace`
+-----------------------------
+
+An XSD element may have a `target namespace`_ assigned.
+
+If an XSD element has a `target namespace`_ assigned then an XML element
+corresponding to this XSD element must belong to that namespace.
+
+If an XSD element does not have a `target namespace`_ assigned then an XML
+element corresponding to this XSD element must not belong to any namespace.
+
+Whether an XSD element has a `target namespace`_ assigned depends on whether it
+is `qualified`_ or not, whether it is a reference and on its or its referenced
+element schema's ``targetNamespace`` attribute:
+
+* an `unqualified`_ element never has a `target namespace`_ assigned
+* a non-reference `qualified`_ element collects its `target namespace`_ from its
+  schema's non-empty ``targetNamespace`` attribute or has no `target namespace`_
+  if its schema does not have a non-empty ``targetNamespace`` attribute value
+  specified
+* a reference element (such elements are always `qualified`_) collects its
+  `target namespace`_ in the same way but used its referenced element schema
+  instead of its own
diff --git a/notes/xsd_types.rst b/notes/xsd_types.rst
new file mode 100644
index 0000000..55f8447
--- /dev/null
+++ b/notes/xsd_types.rst
@@ -0,0 +1,38 @@
+==========================
+Modeling XSD types in suds
+==========================
+:Authors: Jurko Gospodnetiæ
+:Date: 2014-01-29
+
+XSD types can be one of the following:
+
+built-in
+  Defined in the XSD specification.
+
+user defined
+  Constructed by composing built-in and other user defined types according to
+  rules defined in the XSD specification.
+
+XSD elements represent specific input/output data in suds. Each XSD element is
+of a particular XSD type.
+
+In suds an XSD element can be *resolved* - process returning an object
+representing the element's type.
+
+Built-in types are represented in suds using different ``XType`` classes defined
+in the ``suds.xsd.sxbuiltin module``, e.g. ``XFloat``, ``XInteger`` or
+``XString``. Such classes define their ``translate()`` methods, implementing
+transformations between Python objects and their XSD type value representations,
+i.e. their representation as used in SOAP XML documents.
+
+User code can defined additional or replacement ``XType`` classes and register
+them with suds for a specific XSD type using the
+``suds.xsd.sxbuiltin.Factory.maptag()`` method. This way suds can be extended
+with additional functionality.
+
+Example:
+--------
+
+You can define a replacement ``XFloat`` implementation allowing you to pass a
+``fractions.Fraction`` arguments to a web service operation taking an
+``xsd:float`` input parameter.
diff --git a/setup.cfg b/setup.cfg
index 36ee378..b50d1a2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,14 +1,95 @@
-[install]
-optimize = 1
-
-[sdist]
-formats = bztar,zip
-
-[pytest]
-norecursedirs = .git .hg .svn build dist
-
-[egg_info]
-tag_build = 
-tag_date = 0
-tag_svn_revision = 0
-
+[install]
+optimize = 1
+
+[sdist]
+formats = bztar,zip
+
+[env:2.4.4 x86]
+command = py244.cmd
+
+[env:2.4.3 x86]
+command = py243.cmd
+
+[env:2.5.4 x64]
+command = py254.cmd
+
+[env:2.5.4 x86]
+command = py254_x86.cmd
+
+[env:2.6.6 x64]
+command = py266.cmd
+
+[env:2.6.6 x86]
+command = py266_x86.cmd
+
+[env:2.7.6 x64]
+command = py276.cmd
+
+[env:2.7.6 x86]
+command = py276_x86.cmd
+
+[env:2.7.7 x64]
+command = py277.cmd
+
+[env:2.7.7 x86]
+command = py277_x86.cmd
+
+[env:3.1.3 x64]
+command = py313.cmd
+
+[env:3.2.5 x64]
+command = py325.cmd
+
+[env:3.2.5 x86]
+command = py325_x86.cmd
+
+[env:3.3.3 x64]
+command = py333.cmd
+
+[env:3.3.3 x86]
+command = py333_x86.cmd
+
+[env:3.3.5 x64]
+command = py335.cmd
+
+[env:3.3.5 x86]
+command = py335_x86.cmd
+
+[env:3.4.0 x64]
+command = py340.cmd
+
+[env:3.4.0 x86]
+command = py340_x86.cmd
+
+[env:3.4.1 x64]
+command = py341.cmd
+
+[env:3.4.1 x86]
+command = py341_x86.cmd
+
+[pytest]
+norecursedirs = .git .hg .svn build dist
+python_files = test_*.py *__pytest_assert_rewrite_needed.py
+
+[setup base environments - actions]
+report environment configuration = False
+report raw environment scan results = False
+setup setuptools = IfNeeded
+download installations = IfNeeded
+install environments = Yes
+
+[setup base environments - folders]
+installation cache = SCRIPT-FOLDER//__down load__
+pip download cache = %(installation cache)s/__pip download cache__
+ez_setup folder = PROJECT-FOLDER//.
+
+[setup base environments - reuse pre-installed setuptools]
+old = False
+best = True
+future = True
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
index 12bc3af..8abea0d 100644
--- a/setup.py
+++ b/setup.py
@@ -1,73 +1,381 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Main installation and project management script for this project.
 
-# Automatically download & install an appropriate setuptools version if needed.
-import ez_setup
-ez_setup.use_setuptools()
+Attempts to use setuptools if available, and even attempts to install it
+automatically if it is not, downloading it from PyPI if needed. However, its
+main functionality will function just fine without setuptools as well. Having
+setuptools available provides us with the following benefits:
+  - simpler py2to3/distutils integration
+  - setup.py 'egg_info' command constructing the project's metadata
+  - setup.py 'develop' command deploying the project in 'development mode',
+    thus making it available on sys.path, yet still editable directly in its
+    source checkout - equivalent to running a pip based installation using
+    'easy_install -e' or 'pip install -e'
+  - setup.py 'test' command as a standard way to run the project's test suite
+  - when running the installation directly from the source tree setup.py
+    'install' command:
+      - automatically installs the project's metadata so package management
+        tools like pip recognize the installation correctly, e.g. this allows
+        using 'pip uninstall' to undo the installation
+      - package installed as a zipped egg by default
 
-# 'setuptools' related packages.
-import pkg_resources
-from setuptools import setup, find_packages
+"""
+
+import sys
+if sys.version_info < (2, 4):
+    print("ERROR: Python 2.4+ required")
+    sys.exit(-2)
+if (3,) <= sys.version_info < (3, 1):
+    print("ERROR: Python 3.0 not supported - please use Python 3.1+ instead")
+    sys.exit(-2)
 
 import os
 import os.path
-import sys
+import re
 
+# Workaround for a Python issue detected with Python 3.1.3 when running our
+# pytest based 'setup.py test' command. At the end of the test run, Python
+# would report an error:
+#
+#   > Error in atexit._run_exitfuncs:
+#   > TypeError: print_exception(): Exception expected for value, str found
+#
+# Workaround found suggested by Richard Oudkerk in Python's issue tracker at:
+#   http://bugs.python.org/issue15881#msg170215
+#
+# The error is caused by two chained Python bugs:
+#  1. The multiprocessing module seems to call its global cleanup function only
+#     after all of its globals have already been released, thus raising an
+#     exception.
+#  2. atexit exception handling implementation is not prepared to handle and
+#     correctly report the exception as raised by the multiprocessing module
+#     cleanup.
+if (3,) <= sys.version_info < (3, 2):
+    import multiprocessing
+    del multiprocessing
 
-def read_python_code(filename):
-    "Returns the given Python source file's compiled content."
-    file = open(filename, "rt")
-    try:
-        source = file.read()
-    finally:
-        file.close()
-    #   Python 2.6 and below did not support passing strings to exec() &
-    # compile() functions containing line separators other than '\n'. To
-    # support them we need to manually make sure such line endings get
-    # converted even on platforms where this is not handled by native text file
-    # read operations.
-    source = source.replace("\r\n", "\n").replace("\r", "\n")
-    return compile(source, filename, "exec")
 
+# -----------------------------------------------------------------------------
+# Global variables.
+# -----------------------------------------------------------------------------
+
+distutils_cmdclass = {}
+extra_setup_params = {}
+
+# Hardcoded configuration.
+attempt_to_use_setuptools = True
+attempt_to_install_setuptools = True
+
+
+# -----------------------------------------------------------------------------
+# Detect the setup.py environment - current & script folder.
+# -----------------------------------------------------------------------------
 
 # Setup documentation incorrectly states that it will search for packages
 # relative to the setup script folder by default when in fact it will search
 # for them relative to the current working folder. It seems avoiding this
 # problem cleanly and making the setup script runnable with any current working
-# folder would require better setup() support.
+# folder would require better distutils and/or setuptools support.
 # Attempted alternatives:
 #   * Changing the current working folder internally makes any passed path
 #     parameters be interpreted relative to the setup script folder when they
 #     should be interpreted relative to the initial current working folder.
-#   * Passing the script folder as setup() & find_packages() function
-#     parameters makes the final installed distribution contain the absolute
-#     package source location information and not include some other meta-data
-#     package information as well.
+#   * Passing the script folder to setup() using the parameter
+#       package_dir={"": script_folder}
+#     makes the setup 'build' command work from any folder but 'egg-info'
+#     (together with any related commands) still fails.
 script_folder = os.path.realpath(os.path.dirname(__file__))
 current_folder = os.path.realpath(os.getcwd())
 if script_folder != current_folder:
     print("ERROR: Suds library setup script needs to be run from the folder "
         "containing it.")
-    print()
+    print("")
     print("Current folder: %s" % current_folder)
     print("Script folder: %s" % script_folder)
     sys.exit(-2)
 
+
+# -----------------------------------------------------------------------------
+# Import suds_devel module shared between setup & development scripts.
+# -----------------------------------------------------------------------------
+
+tools_folder = os.path.join(script_folder, "tools")
+sys.path.insert(0, tools_folder)
+import suds_devel
+sys.path.pop(0)
+
+from suds_devel.requirements import (check_Python24_pytest_requirements,
+    pytest_requirements, six_requirements)
+
+
+# -----------------------------------------------------------------------------
+# Attempt to use setuptools for this installation.
+# -----------------------------------------------------------------------------
+# setuptools brings us several useful features (see the main setup script
+# docstring) but if it causes problems for our users or even only
+# inconveniences them, they tend to complain about why we are using setuptools
+# at all. Therefore we try to use setuptools as silently as possible, with a
+# clear error message displayed to the user, but only in cases when we
+# absolutely need setuptools.
+#
+# Setuptools usage logic:
+# 1. attempt to use a preinstalled setuptools version
+# 2. if a preinstalled setuptools version is not available, attempt to install
+#    and use the most recent tested compatible setuptools release, possibly
+#    downloading the installation from PyPI into the current folder
+# 3. if we still do not have setuptools available, fall back to using distutils
+#
+# Note that we have made a slight trade-off here and chose to reuse an existing
+# setuptools installation if available instead of always downloading and
+# installing the most recent compatible setuptools release.
+#
+# Alternative designs and rationale for selecting the current design:
+#
+# distutils/setuptools usage:
+#   * always use distutils
+#       - misses out on all the setuptools features
+#   * use setuptools if available, or fall back to using distutils
+#       - chosen design
+#       - gets us setuptools features if available, with clear end-user error
+#         messages in case an operation is triggered that absolutely requires
+#         setuptools to be available
+#       - see below for notes on different setuptools installation alternatives
+#   * always use setuptools
+#       - see below for notes on different setuptools installation alternatives
+#
+# setuptools installation:
+#   * expect setuptools to be preinstalled
+#       - if not available, burden the user with installing setuptools manually
+#       - see below for notes on using different setuptools versions
+#   * use preinstalled setuptools if possible, or fall back to installing it
+#     on-demand
+#       - chosen design
+#       - see below for notes on using different setuptools versions
+#       - automated setuptools installations, and especially in-place upgrades,
+#         can fail for various reasons (see below)
+#       - reduces the risk of a stalled download stalling the whole setup
+#         operation, e.g. because of an unavailable or unresponsive DNS server
+#   * always install setuptools
+#       - automated setuptools installations, and especially in-place upgrades,
+#         can fail for various reasons (see below)
+#       - user has no way to avoid setuptools installation issues by installing
+#         setuptools himself, which would force us to make our setuptools
+#         installation support universally applicable and that is just not
+#         possible, e.g. users might be wanting to use customized package
+#         management or their own package index instead of PyPI.
+#
+# setuptools version usage:
+#   - basic problem here is that our project can be and has been tested only
+#     with certain setuptools versions
+#   - using a different version may have issues causing our setup procedures to
+#     fail outside our control, either due to a setuptools bug or due to an
+#     incompatibility between our implementation and the used setuptools
+#     version
+#   - some setuptools releases have known regressions, e.g. setuptools 3.0
+#     which is documented to fail on Python 2.6 due to an accidental backward
+#     incompatibility corrected in the setuptools 3.1 release
+#   * allow using any setuptools version
+#       - chosen design
+#       - problems caused by incompatible setuptools version usage are
+#         considered highly unlikely and can therefore be patched up as needed
+#         when and if they appear
+#       - users will most likely not have a setuptools version preinstalled
+#         into their Python environment that is incompatible with that
+#         environment
+#       - if there is no setuptools version installed, our setup will attempt
+#         to install the most recent tested setuptools release
+#   * allow using only tested setuptools versions or possibly more recent ones
+#       - unless we can automatically install a suitable setuptools version, we
+#         will need to burden the user with installing it manually or fall back
+#         to using distutils
+#       - automated setuptools installations, and especially in-place upgrades,
+#         can fail for various reasons (see below)
+#       - extra implementation effort required compared to the chosen design,
+#         with no significant benefit
+#
+# Some known scenarios causing automated setuptools installation failures:
+#   * Download failures, e.g. because user has no access to PyPI.
+#   * In-place setuptool upgrades can fail due to a known setuptools issue (see
+#     'https://bitbucket.org/pypa/setuptools/issue/168') when both the original
+#     and the new setuptools version is installed as a zipped egg distribution.
+#     Last seen using setuptools 3.4.4.
+#   * If the Python environment running our setup has already loaded setuptools
+#     packages, then upgrading that installation in-place will fail with an
+#     error message instructing the user to do the upgrade manually.
+#   * When installing our project using pip, pip will load setuptools
+#     internally, and it typically uses an older setuptools version which can
+#     trigger the in-place upgrade failure as described above. What is worse,
+#     we ignore this failure, we run into the following combination problem:
+#       * pip calls our setup twice - once to collect the package requirement
+#         information and once to perform the actual installation, and we do
+#         not want to display multiple potentially complex error messages to
+#         user for what is effectively the same error.
+#       * Since we can not affect how external installers call our setup, to
+#         avoid this we would need to either:
+#           * Somehow cache the information that we already attempted and
+#             failed to upgrade setuptools (complicated + possibly not robust).
+#           * Patch the setuptools installation script to not display those
+#             error messages (we would prefer to not be forced to maintain our
+#             own patches for this script and use it as is).
+#           * Avoid the issue by never upgrading an existing setuptools
+#             installation (chosen design).
+
+def acquire_setuptools_setup():
+    if not attempt_to_use_setuptools:
+        return
+
+    def import_setuptools_setup():
+        try:
+            from setuptools import setup
+        except ImportError:
+            return
+        return setup
+
+    setup = import_setuptools_setup()
+    if setup or not attempt_to_install_setuptools:
+        return setup
+    if (3,) <= sys.version_info[:2] < (3, 2):
+        # Setuptools contains a test module with an explicitly specified UTF-8
+        # BOM, which is not supported by Python's py2to3 tool prior to Python
+        # 3.2 (see Python issue #7313). Setuptools can still be installed
+        # manually using its ez_setup.py installer script (it will report and
+        # ignore the error), but if we use the ez_setup.use_setuptools()
+        # programmatic setup invocation from here - it will fail.
+        #
+        # There are several things that could be done here - patches welcome if
+        # anyone actually needs them:
+        #  - the issue could be worked around by running the setuptools
+        #    installation as a separate process in this case
+        #  - warning display could be more cleanly integrated into distutils
+        #    command execution process so the warning does not get displayed
+        #    if setuptools would not actually be useful, e.g. if user just ran
+        #    our setup script with no command or with the --help option
+        print("---")
+        print("WARNING: can not install setuptools automatically using Python "
+            "3.0 & 3.1")
+        print("WARNING: if needed, install setuptools manually before running "
+            "this installation using Python prior to version 3.2")
+        print("---")
+        return
+    import suds_devel.ez_setup_versioned
+    ez_setup = suds_devel.ez_setup_versioned.import_module()
+    try:
+        # Since we know there is no setuptools package in the current
+        # environment, this will:
+        # 1. download a setuptools source distribution to the current folder
+        # 2. prepare an installable setuptools egg distribution in the current
+        #    folder
+        # 3. schedule for the prepared setuptools distribution to be installed
+        #    together with our package (if our package is getting installed at
+        #    all and setup has not been called for some other purpose, e.g.
+        #    displaying its help information or running a non-install related
+        #    setup command)
+        ez_setup.use_setuptools()
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        return
+    return import_setuptools_setup()
+
+setup = acquire_setuptools_setup()
+using_setuptools = bool(setup)
+if not using_setuptools:
+    # Fall back to using distutils.
+    from distutils.core import setup
+
+
+# -----------------------------------------------------------------------------
+# Support functions.
+# -----------------------------------------------------------------------------
+
+def read_python_code(filename):
+    "Returns the given Python source file's compiled content."
+    file = open(filename, "rt")
+    try:
+        source = file.read()
+    finally:
+        file.close()
+    # Python 2.6 and below did not support passing strings to exec() &
+    # compile() functions containing line separators other than '\n'. To
+    # support them we need to manually make sure such line endings get
+    # converted even on platforms where this is not handled by native text file
+    # read operations.
+    source = source.replace("\r\n", "\n").replace("\r", "\n")
+    return compile(source, filename, "exec")
+
+def recursive_package_list(*packages):
+    """
+    Returns a list of all the given packages and all their subpackages.
+
+    Given packages are expected to be found relative to this script's location.
+
+    Subpackages are detected by scanning the given packages' subfolder
+    hierarchy for any folders containing the '__init__.py' module. As a
+    consequence, namespace packages are not supported.
+
+    This is our own specialized setuptools.find_packages() replacement so we
+    can avoid the setuptools dependency.
+
+    """
+    result = set()
+    todo = []
+    for package in packages:
+        folder = os.path.join(script_folder, *package.split("."))
+        if not os.path.isdir(folder):
+            raise Exception("Folder not found for package '%s'." % (package,))
+        todo.append((package, folder))
+    while todo:
+        package, folder = todo.pop()
+        if package in result:
+            continue
+        result.add(package)
+        for subitem in os.listdir(folder):
+            subpackage = ".".join((package, subitem))
+            subfolder = os.path.join(folder, subitem)
+            if not os.path.isfile(os.path.join(subfolder, "__init__.py")):
+                continue
+            todo.append((subpackage, subfolder))
+    return list(result)
+
+# Shamelessly stolen from setuptools project's pkg_resources module.
+def safe_version(version_string):
+    """
+    Convert an arbitrary string to a standard version string
+
+    Spaces become dots, and all other non-alphanumeric characters become
+    dashes, with runs of multiple dashes condensed to a single dash.
+
+    """
+    version_string = version_string.replace(" ", ".")
+    return re.sub("[^A-Za-z0-9.]+", "-", version_string)
+
+def unicode2ascii(unicode):
+    """Convert a unicode string to its approximate ASCII equivalent."""
+    return unicode.encode("ascii", 'xmlcharrefreplace').decode("ascii")
+
+
+# -----------------------------------------------------------------------------
+# Load the suds library version information.
+# -----------------------------------------------------------------------------
+
 # Load the suds library version information directly into this module without
 # having to import the whole suds library itself. Importing the suds package
 # would have caused problems like the following:
@@ -78,32 +386,274 @@ if script_folder != current_folder:
 #     forcing the user to install them manually (since the setup procedure that
 #     is supposed to install them automatically will not be able to run unless
 #     they are already installed).
-#   We execute explicitly compiled source code instead of having the exec()
+# We execute explicitly compiled source code instead of having the exec()
 # function compile it to get a better error messages. If we used exec() on the
 # source code directly, the source file would have been listed as just
 # '<string>'.
-exec(read_python_code(os.path.join("suds", "version.py")))
+exec(read_python_code(os.path.join(script_folder, "suds", "version.py")))
 
-extra_setup_params = {}
-extra_setup_cmdclass = {}
+
+# -----------------------------------------------------------------------------
+# Custom setup.py 'test' command for running the project test suite.
+# -----------------------------------------------------------------------------
+# pytest and any of its requirements not already installed in the target Python
+# environment will be automatically downloaded from PyPI and installed into the
+# current folder as zipped egg distributions.
+#
+# Requirements:
+#   - setup must be using setuptools
+#   - if running Python version prior to 2.5, a suitable pytest version must
+#     already be installed and will not be installed on demand (see the related
+#     embedded code comment below for more detailed information)
+#
+# If the requirements are not met, the command simply reports an end-user error
+# message explaining why the test functionality is unavailable.
+#
+# Since Python's distutils framework does not allow passing all received
+# command-line arguments to its commands, it does not seem easy to customize
+# how pytest runs its tests this way. To have better control over this, user
+# should run the pytest on the target source tree directly, possibly after
+# first building a temporary one to work around problems like Python 2/3
+# compatibility.
+
+def test_requirements():
+    """
+    Return test requirements for the 'test' command or an error string.
+
+    An error is reported if the requirements can not be satisfied for some
+    reason.
+
+    Exact required packages and their versions vary depending on our target
+    Python environment version as pytest dropped backward compatibility support
+    for some of the Python versions we still support in this project.
+
+    """
+    if not using_setuptools:
+        return "test command not available without setuptools"
+
+    include_pytest_requirements = True
+
+    if sys.version_info < (2, 5):
+        # pytest requirements can not be installed automatically by this setup
+        # script under Python 2.4.x environment. Specific pytest & py library
+        # package version combination that we found working in Python 2.4.x
+        # environments does not formally satisfy pytest requirements, and we
+        # found no way to make setuptools' test command succeed when this
+        # script installs packages that do not have all their formal
+        # requirements satisfied.
+        have_pytest, have_py = check_Python24_pytest_requirements()
+        if not have_pytest:
+            return "compatible preinstalled pytest needed prior to Python 2.5"
+        if not have_py:
+            return "compatible preinstalled py needed prior to Python 2.5"
+
+        # We must not explicitly specify pytest requirements when running the
+        # tests using a Python 2.4.x environment as the only way we found we
+        # can run our tests there is to use formally incompatible pytest & py
+        # packages. Explicitly specifying pytest requirements here would then
+        # cause setuptols to verify those requirements prior to running our
+        # test suite.
+        include_pytest_requirements = False
+
+    if ((3,) <= sys.version_info < (3, 2, 3)):
+        # Python 3.x versions prior to Python 3.2.3 have a bug in their inspect
+        # module causing inspect.getmodule() calls to fail if some module lazy
+        # loads other modules when some of its attributes are accessed. For
+        # more detailed information see Python development issue #13487
+        # (http://bugs.python.org/issue13487).
+        #
+        # This occurs when using setuptools to install our project into a
+        # Python 3.1 environment. There the py.error module seems to do such
+        # lazy loading. Forcing that module to be loaded here, before the
+        # setuptools installation procedure, avoids the issue.
+        try:
+            import py.error
+            py.error.__attribute_access_to_force_this_module_to_lazy_load__
+        except (AttributeError, ImportError):
+            pass
+
+    # When using Python 2.5 on Windows, if setuptools chooses to install the
+    # colorama package (pytest requirement on Windows) older than version
+    # 0.1.11, running our 'setup.py test' command may show benign error
+    # messages caused by some colorama atexit handlers getting triggered
+    # multiple times. There are no adverse effects to this and the issue only
+    # occurs if the package is not already installed and it is the 'setup.py
+    # test' command that is installing it. The issue has been fixed by the next
+    # Python 2.5 compatible colorama 0.3.2 release.
+    result = []
+    if include_pytest_requirements:
+        result.extend(pytest_requirements())
+    result.extend(six_requirements())
+    return result
+
+test_error = None
+tests_require = test_requirements()
+if isinstance(tests_require, str):
+    test_error = tests_require
+else:
+    extra_setup_params["tests_require"] = tests_require
+
+if test_error:
+    import distutils.cmd
+    import distutils.errors
+
+    class TestCommand(distutils.cmd.Command):
+        description = test_error
+        user_options = []
+        def initialize_options(self):
+            pass
+        def finalize_options(self):
+            pass
+        def run(self):
+            raise distutils.errors.DistutilsPlatformError(self.description)
+else:
+    from setuptools.command.test import (normalize_path as _normalize_path,
+        test as _test)
+
+    class TestCommand(_test):
+        # Derived from setuptools.command.test.test for its
+        # with_project_on_sys_path() method.
+
+        # The test build can not be done in-place with Python 3+ as it requires
+        # py2to3 conversion which we do not want modifying our original project
+        # sources.
+        if sys.version_info < (3,):
+            description = "run pytest based unit tests after an in-place build"
+        else:
+            description = "run pytest based unit tests after a build"
+
+        # Override base class's command-line options.
+        #TODO: pytest argument passing support could be improved if we could
+        # get distutils/setuptools to pass all unrecognized command-line
+        # parameters to us instead of complaining about them.
+        user_options = [("pytest-args=", "a", "arguments to pass to pytest "
+            "(whitespace separated, whitespaces in arguments not supported)")]
+
+        def initialize_options(self):
+            self.pytest_args = None
+
+        def finalize_options(self):
+            self.test_args = []
+            if self.pytest_args is not None:
+                self.test_args = self.pytest_args.split()
+
+        def run(self):
+            # shamelessly lifted from setuptools.command.test.test.run()
+            dist = self.distribution
+            if dist.install_requires:
+                dist.fetch_build_eggs(dist.install_requires)
+            if dist.tests_require:
+                dist.fetch_build_eggs(dist.tests_require)
+
+            cmd = self._test_cmd_string()
+            if self.dry_run:
+                self.announce("skipping '%s' (dry run)" % (cmd,))
+            else:
+                self.announce("running '%s'" % (cmd,))
+                self.with_project_on_sys_path(self.run_tests)
+
+        def run_tests(self):
+            import pytest
+            sys.exit(pytest.main(self.test_args))
+
+        def _test_cmd_string(self):
+            parts = ["pytest"]
+            if self.pytest_args:
+                parts.append(self.pytest_args)
+            return " ".join(parts)
+
+distutils_cmdclass["test"] = TestCommand
+
+
+# -----------------------------------------------------------------------------
+# Mark the original suds project as obsolete.
+# -----------------------------------------------------------------------------
 
 if sys.version_info >= (2, 5):
     # distutils.setup() 'obsoletes' parameter not introduced until Python 2.5.
     extra_setup_params["obsoletes"] = ["suds"]
 
-if sys.version_info >= (3, 0):
-    extra_setup_params["use_2to3"] = True
 
-    #   Teach Python's urllib lib2to3 fixer that the old urllib2.__version__
-    # data member is now stored in the urllib.request module.
+# -----------------------------------------------------------------------------
+# Integrate py2to3 into our build operation.
+# -----------------------------------------------------------------------------
+
+if sys.version_info >= (3,):
+    # Integrate the py2to3 step into our build.
+    if using_setuptools:
+        extra_setup_params["use_2to3"] = True
+    else:
+        from distutils.command.build_py import build_py_2to3
+        distutils_cmdclass["build_py"] = build_py_2to3
+
+    # Teach Python's urllib lib2to3 fixer that the old urllib2.__version__ data
+    # member is now stored in the urllib.request module.
     import lib2to3.fixes.fix_urllib
     for x in lib2to3.fixes.fix_urllib.MAPPING["urllib2"]:
         if x[0] == "urllib.request":
             x[1].append("__version__")
-            break;
+            break
+
+
+# -----------------------------------------------------------------------------
+# Avoid setup warnings when constructing a list of all project sources.
+# -----------------------------------------------------------------------------
+# Part of this workaround implemented and part in the project's MANIFEST.in
+# template. See a related comment in MANIFEST.in for more detailed information.
 
-#   Wrap long_description at 72 characters since PKG-INFO package distribution
-# metadata file stores this text with an 8 space indentation.
+dummy_tools_folder = os.path.join(tools_folder, "__dummy__")
+dummy_tools_file = os.path.join(dummy_tools_folder, "readme.txt")
+try:
+    if not os.path.isdir(dummy_tools_folder):
+        os.mkdir(dummy_tools_folder)
+    if not os.path.isfile(dummy_tools_file):
+        f = open(dummy_tools_file, "w")
+        try:
+            f.write("""\
+Dummy empty folder added as a part of a patch to silence setup.py warnings when
+determining which files belong to the project. See a related comment in the
+project's MANIFEST.in template for more detailed information.
+
+Both the folder and this file have been generated by the project's setup.py
+script and should not be placed under version control.
+""")
+        finally:
+            f.close()
+except EnvironmentError:
+    # Something went wrong attempting to construct the dummy file. Ah well, we
+    # gave it our best. Continue on with possible spurious warnings.
+    pass
+
+
+# -----------------------------------------------------------------------------
+# Set up project metadata and run the actual setup.
+# -----------------------------------------------------------------------------
+# Package meta-data needs may be specified as:
+#  * Python 2.x - UTF-8 encoded bytes
+#  * Python [2.6, 3.0> - unicode string
+#      - unicode strings containing non-ASCII characters supported since Python
+#        commit 4c683ec4415b3c4bfbc7fe7a836b949cb7beea03
+#  * Python [3.0, 3.2.2>
+#      - may only contain ASCII characters due to a distutils bug (given input
+#        can only be a unicode string and is encoded using the user's default
+#        system code-page, e.g. typically CP1250 on eastern European Windows,
+#        CP1252 on western European Windows, UTF-8 on Linux or any other)
+#      - setuptools 3.5 works around the issue by overriding relevant distutils
+#        functionality, allowing the use of non-ASCII characters, but only for
+#        Python 3.1
+#  * Python 3.2.2+ - unicode string
+#      - unicode strings containing non-ASCII characters supported since Python
+#        commit fb4d2e6d393e96baac13c4efc216e361bf12c293
+
+can_not_use_non_ASCII_meta_data = (3,) <= sys.version_info < (3, 2, 2)
+if (can_not_use_non_ASCII_meta_data and using_setuptools and
+        sys.version_info[:2] == (3, 1)):
+    from setuptools import __version__ as setuptools_version
+    from pkg_resources import parse_version as pv
+    can_not_use_non_ASCII_meta_data = pv(setuptools_version) < pv("3.5")
+
+# Wrap long_description at 72 characters since the PKG-INFO package
+# distribution metadata file stores this text with an 8 space indentation.
 long_description = """
 ---------------------------------------
 Lightweight SOAP client (Jurko's fork).
@@ -124,40 +674,15 @@ if it ever gets revived again.
 """
 
 package_name = "suds-jurko"
-version_tag = pkg_resources.safe_version(__version__)
+version_tag = safe_version(__version__)
 project_url = "http://bitbucket.org/jurko/suds"
 base_download_url = project_url + "/downloads"
 download_distribution_name = "%s-%s.tar.bz2" % (package_name, version_tag)
 download_url = "%s/%s" % (base_download_url, download_distribution_name)
 
-# Support for integrating running the project' pytest based test suite directly
-# into this setup script so the test suite can be run by 'setup.py test'. Since
-# Python's distutils framework does not allow passing all received command-line
-# arguments to its commands, it does not seem easy to customize how pytest runs
-# its tests this way. To have better control over this, user should run the
-# pytest on the target source tree directly, possibly after first building a
-# temporary one to work around problems like Python 2/3 compatibility.
-import setuptools.command.test
-class PyTest(setuptools.command.test.test):
-    def finalize_options(self):
-        setuptools.command.test.test.finalize_options(self)
-        self.test_args = []
-        self.test_suite = True
-    def run_tests(self):
-        # Make sure the tests are run on the correct test sources. E.g. when
-        # using Python 3, the tests need to be run in the temporary build
-        # folder where they have been previously processed using py2to3.
-        # Running them directly on the original source tree would fail due to
-        # Python 2/3 source code incompatibility.
-        ei_cmd = self.get_finalized_command("egg_info")
-        build_path = setuptools.command.test.normalize_path(ei_cmd.egg_base)
-        test_args = ["--pyargs", build_path]
-        import pytest
-        errno = pytest.main(test_args)
-        sys.exit(errno)
-extra_setup_params.update(tests_require=["pytest"])
-extra_setup_cmdclass.update(test=PyTest)
-
+maintainer="Jurko Gospodnetić"
+if can_not_use_non_ASCII_meta_data:
+    maintainer = unicode2ascii(maintainer)
 
 setup(
     name=package_name,
@@ -167,27 +692,14 @@ setup(
     keywords=["SOAP", "web", "service", "client"],
     url=project_url,
     download_url=download_url,
-    packages=find_packages(),
-
-    # 'maintainer' will be listed as the distribution package author.
-    # Warning: Due to a 'distribute' package defect when used with Python 3
-    # (verified using 'distribute' package version 0.6.25), given strings must
-    # be given using ASCII characters only. This is needed because 'distribute'
-    # stores the strings by doing a simple write to a PKG-INFO file opened as a
-    # 'default text file' thus attempting to encode the given characters using
-    # the user's default system code-page, e.g. typically CP1250 on eastern
-    # European Windows, CP1252 on western European Windows, UTF-8 on Linux or
-    # any other.
-    #
-    # 'distribute' package merged back with the 'setuptools' package in the
-    # setuptools 0.7 release but we have not yet checked whether this bug has
-    # been corrected there or not.
+    packages=recursive_package_list("suds"),
+
     author="Jeff Ortel",
     author_email="jortel at redhat.com",
-    maintainer="Jurko Gospodnetic",
+    maintainer=maintainer,
     maintainer_email="jurko.gospodnetic at pke.hr",
 
-    #   See PEP-301 for the classifier specification. For a complete list of
+    # See PEP-301 for the classifier specification. For a complete list of
     # available classifiers see
     # 'http://pypi.python.org/pypi?%3Aaction=list_classifiers'.
     classifiers=["Development Status :: 5 - Production/Stable",
@@ -203,19 +715,18 @@ setup(
         "Programming Language :: Python :: 2.6",
         "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.0",
         "Programming Language :: Python :: 3.1",
         "Programming Language :: Python :: 3.2",
         "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
         "Topic :: Internet"],
 
-    #   PEP-314 states that if possible license & platform should be specified
+    # PEP-314 states that, if possible, license & platform should be specified
     # using 'classifiers'.
     license="(specified using classifiers)",
     platforms=["(specified using classifiers)"],
 
-    # Register custom distutils commands.
-    cmdclass=extra_setup_cmdclass,
+    # Register distutils command customizations.
+    cmdclass=distutils_cmdclass,
 
-    **extra_setup_params
-)
+    **extra_setup_params)
diff --git a/suds/__init__.py b/suds/__init__.py
index 1f8a6d9..c45ca33 100644
--- a/suds/__init__.py
+++ b/suds/__init__.py
@@ -1,21 +1,21 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Suds is a lightweight SOAP Python client providing a Web Service proxy.
+Lightweight SOAP Python client providing a Web Service proxy.
+
 """
 
 import sys
@@ -34,45 +34,33 @@ from version import __build__, __version__
 
 class MethodNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, u"Method not found: '%s'" % name)
+        Exception.__init__(self, u"Method not found: '%s'" % (name,))
 
 class PortNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, u"Port not found: '%s'" % name)
+        Exception.__init__(self, u"Port not found: '%s'" % (name,))
 
 class ServiceNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, u"Service not found: '%s'" % name)
+        Exception.__init__(self, u"Service not found: '%s'" % (name,))
 
 class TypeNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, u"Type not found: '%s'" % tostr(name))
+        Exception.__init__(self, u"Type not found: '%s'" % (tostr(name),))
 
 class BuildError(Exception):
-    msg = """
-        An error occurred while building an instance of (%s). As a result the
-        object you requested could not be constructed. It is recommended that
-        you construct the type manually using a Suds object. Please open a
-        ticket with a description of this error.
-        Reason: %s
-        """
     def __init__(self, name, exception):
-        Exception.__init__(self, BuildError.msg % (name, exception))
-
-class SoapHeadersNotPermitted(Exception):
-    msg = """
-        Method (%s) was invoked with SOAP headers. The WSDL does not define
-        SOAP headers for this method. Retry without the soapheaders keyword
-        argument.
-        """
-    def __init__(self, name):
-        Exception.__init__(self, self.msg % name)
+        Exception.__init__(self, u"An error occurred while building an "
+            "instance of (%s). As a result the object you requested could not "
+            "be constructed. It is recommended that you construct the type "
+            "manually using a Suds object. Please open a ticket with a "
+            "description of this error. Reason: %s" % (name, exception))
 
 class WebFault(Exception):
     def __init__(self, fault, document):
-        if hasattr(fault, 'faultstring'):
+        if hasattr(fault, "faultstring"):
             Exception.__init__(self, u"Server raised fault: '%s'" %
-                fault.faultstring)
+                (fault.faultstring,))
         self.fault = fault
         self.document = document
 
@@ -93,60 +81,44 @@ class Repr:
 #
 
 class null:
-    """
-    The I{null} object.
-    Used to pass NULL for optional XML nodes.
-    """
+    """I{null} object used to pass NULL for optional XML nodes."""
     pass
 
 def objid(obj):
-    return obj.__class__.__name__ + ':' + hex(id(obj))
+    return obj.__class__.__name__ + ":" + hex(id(obj))
 
 def tostr(object, encoding=None):
-    """ get a unicode safe string representation of an object """
+    """Get a unicode safe string representation of an object."""
     if isinstance(object, basestring):
         if encoding is None:
             return object
-        else:
-            return object.encode(encoding)
+        return object.encode(encoding)
     if isinstance(object, tuple):
-        s = ['(']
+        s = ["("]
         for item in object:
-            if isinstance(item, basestring):
-                s.append(item)
-            else:
-                s.append(tostr(item))
-            s.append(', ')
-        s.append(')')
-        return ''.join(s)
+            s.append(tostr(item))
+            s.append(", ")
+        s.append(")")
+        return "".join(s)
     if isinstance(object, list):
-        s = ['[']
+        s = ["["]
         for item in object:
-            if isinstance(item, basestring):
-                s.append(item)
-            else:
-                s.append(tostr(item))
-            s.append(', ')
-        s.append(']')
-        return ''.join(s)
+            s.append(tostr(item))
+            s.append(", ")
+        s.append("]")
+        return "".join(s)
     if isinstance(object, dict):
-        s = ['{']
+        s = ["{"]
         for item in object.items():
-            if isinstance(item[0], basestring):
-                s.append(item[0])
-            else:
-                s.append(tostr(item[0]))
-            s.append(' = ')
-            if isinstance(item[1], basestring):
-                s.append(item[1])
-            else:
-                s.append(tostr(item[1]))
-            s.append(', ')
-        s.append('}')
-        return ''.join(s)
+            s.append(tostr(item[0]))
+            s.append(" = ")
+            s.append(tostr(item[1]))
+            s.append(", ")
+        s.append("}")
+        return "".join(s)
     try:
         return unicode(object)
-    except:
+    except Exception:
         return str(object)
 
 
@@ -165,13 +137,13 @@ class UnicodeMixin(object):
         # For Python 3, __str__() and __unicode__() should be identical.
         __str__ = lambda x: x.__unicode__()
     else:
-        __str__ = lambda x: unicode(x).encode('utf-8')
+        __str__ = lambda x: unicode(x).encode("utf-8")
 
-#   Used instead of byte literals because they are not supported on Python
-# versions prior to 2.6.
-def byte_str(s='', encoding='utf-8', input_encoding='utf-8', errors='strict'):
+# Used instead of byte literals as they are not supported on Python versions
+# prior to 2.6.
+def byte_str(s="", encoding="utf-8", input_encoding="utf-8", errors="strict"):
     """
-    Returns a bytestring version of 's', encoded as specified in 'encoding'.
+    Returns a byte string version of 's', encoded as specified in 'encoding'.
 
     Accepts str & unicode objects, interpreting non-unicode strings as byte
     strings encoded using the given input encoding.
diff --git a/suds/bindings/binding.py b/suds/bindings/binding.py
index 62f641c..5bafaec 100644
--- a/suds/bindings/binding.py
+++ b/suds/bindings/binding.py
@@ -14,7 +14,8 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides classes for (WS) SOAP bindings.
+(WS) SOAP binding classes.
+
 """
 
 from suds import *
@@ -34,25 +35,28 @@ from suds.plugin import PluginContainer
 from copy import deepcopy
 
 
-envns = ('SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/')
+envns = ("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/")
 
 
-class Binding:
+class Binding(object):
     """
     The SOAP binding class used to process outgoing and incoming SOAP messages
     per the WSDL port binding.
+
     @ivar wsdl: The WSDL.
     @type wsdl: L{suds.wsdl.Definitions}
     @ivar schema: The collective schema contained within the WSDL.
     @type schema: L{xsd.schema.Schema}
     @ivar options: A dictionary options.
     @type options: L{Options}
+
     """
 
     def __init__(self, wsdl):
         """
         @param wsdl: A WSDL.
         @type wsdl: L{wsdl.Definitions}
+
         """
         self.wsdl = wsdl
         self.multiref = MultiRef()
@@ -66,34 +70,43 @@ class Binding:
     def unmarshaller(self):
         """
         Get the appropriate schema based XML decoder.
+
         @return: Typed unmarshaller.
         @rtype: L{UmxTyped}
+
         """
         return UmxTyped(self.schema())
 
     def marshaller(self):
         """
         Get the appropriate XML encoder.
+
         @return: An L{MxLiteral} marshaller.
         @rtype: L{MxLiteral}
+
         """
         return MxLiteral(self.schema(), self.options().xstq)
 
     def param_defs(self, method):
         """
         Get parameter definitions.
-        Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject})
+
+        Each I{pdef} is a (I{name}, L{xsd.sxbase.SchemaObject}) tuple.
+
         @param method: A service method.
         @type method: I{service.Method}
         @return: A collection of parameter definitions
-        @rtype: [I{pdef},..]
+        @rtype: [I{pdef},...]
+
         """
-        raise Exception, 'not implemented'
+        raise Exception("not implemented")
 
     def get_message(self, method, args, kwargs):
         """
         Get a SOAP message for the specified method, args and SOAP headers.
+
         This is the entry point for creating an outbound SOAP message.
+
         @param method: The method being invoked.
         @type method: I{service.Method}
         @param args: A list of args for the method invoked.
@@ -102,8 +115,8 @@ class Binding:
         @type kwargs: dict
         @return: The SOAP envelope.
         @rtype: L{Document}
-        """
 
+        """
         content = self.headercontent(method)
         header = self.header(content)
         content = self.bodycontent(method, args, kwargs)
@@ -120,19 +133,21 @@ class Binding:
         """
         Process the I{reply} for the specified I{method} by unmarshalling it
         into into Python object(s).
+
         @param method: The name of the invoked method.
         @type method: str
         @param replyroot: The reply XML root node received after invoking the
             specified method.
-        @type reply: L{Element}
-        @return: The unmarshalled reply.  The returned value is an L{Object} or
+        @type replyroot: L{Element}
+        @return: The unmarshalled reply. The returned value is an L{Object} or
             a I{list} depending on whether the service returns a single object
             or a collection.
         @rtype: L{Object} or I{list}
+
         """
-        soapenv = replyroot.getChild('Envelope', envns)
+        soapenv = replyroot.getChild("Envelope", envns)
         soapenv.promotePrefixes()
-        soapbody = soapenv.getChild('Body', envns)
+        soapbody = soapenv.getChild("Body", envns)
         soapbody = self.multiref.process(soapbody)
         nodes = self.replycontent(method, soapbody)
         rtypes = self.returned_types(method)
@@ -148,47 +163,49 @@ class Binding:
 
     def replylist(self, rt, nodes):
         """
-        Construct a I{list} reply. This mehod is called when it has been
-        detected that the reply is a list.
+        Construct a I{list} reply.
+
+        Called for replies with possible multiple occurrences.
+
         @param rt: The return I{type}.
         @type rt: L{suds.xsd.sxbase.SchemaObject}
         @param nodes: A collection of XML nodes.
         @type nodes: [L{Element},...]
         @return: A list of I{unmarshalled} objects.
         @rtype: [L{Object},...]
+
         """
-        result = []
         resolved = rt.resolve(nobuiltin=True)
         unmarshaller = self.unmarshaller()
-        for node in nodes:
-            sobject = unmarshaller.process(node, resolved)
-            result.append(sobject)
-        return result
+        return [unmarshaller.process(node, resolved) for node in nodes]
 
     def replycomposite(self, rtypes, nodes):
         """
-        Construct a I{composite} reply. This method is called when it has been
-        detected that the reply has multiple root nodes.
+        Construct a I{composite} reply.
+
+        Called for replies with multiple output nodes.
+
         @param rtypes: A list of known return I{types}.
         @type rtypes: [L{suds.xsd.sxbase.SchemaObject},...]
         @param nodes: A collection of XML nodes.
         @type nodes: [L{Element},...]
         @return: The I{unmarshalled} composite object.
         @rtype: L{Object},...
+
         """
         dictionary = {}
         for rt in rtypes:
             dictionary[rt.name] = rt
         unmarshaller = self.unmarshaller()
-        composite = Factory.object('reply')
+        composite = Factory.object("reply")
         for node in nodes:
             tag = node.name
-            rt = dictionary.get(tag, None)
+            rt = dictionary.get(tag)
             if rt is None:
-                if node.get('id') is None:
-                    raise Exception('<%s/> not mapped to message part' % tag)
-                else:
-                    continue
+                if node.get("id") is None:
+                    message = "<%s/> not mapped to message part" % (tag,)
+                    raise Exception(message)
+                continue
             resolved = rt.resolve(nobuiltin=True)
             sobject = unmarshaller.process(node, resolved)
             value = getattr(composite, tag, None)
@@ -210,6 +227,7 @@ class Binding:
         """
         Builds a parameter for the specified I{method} using the parameter
         definition (pdef) and the specified value (object).
+
         @param method: A method name.
         @type method: str
         @param pdef: A parameter definition.
@@ -218,6 +236,7 @@ class Binding:
         @type object: any
         @return: The parameter fragment.
         @rtype: L{Element}
+
         """
         marshaller = self.marshaller()
         content = Content(tag=pdef[0], value=object, type=pdef[1],
@@ -228,6 +247,7 @@ class Binding:
         """
         Builds a soapheader for the specified I{method} using the header
         definition (hdef) and the specified value (object).
+
         @param method: A method name.
         @type method: str
         @param hdef: A header definition.
@@ -236,27 +256,27 @@ class Binding:
         @type object: any
         @return: The parameter fragment.
         @rtype: L{Element}
+
         """
         marshaller = self.marshaller()
         if isinstance(object, (list, tuple)):
-            tags = []
-            for item in object:
-                tags.append(self.mkheader(method, hdef, item))
-            return tags
+            return [self.mkheader(method, hdef, item) for item in object]
         content = Content(tag=hdef[0], value=object, type=hdef[1])
         return marshaller.process(content)
 
     def envelope(self, header, body):
         """
         Build the B{<Envelope/>} for a SOAP outbound message.
+
         @param header: The SOAP message B{header}.
         @type header: L{Element}
         @param body: The SOAP message B{body}.
         @type body: L{Element}
         @return: The SOAP envelope containing the body and header.
         @rtype: L{Element}
+
         """
-        env = Element('Envelope', ns=envns)
+        env = Element("Envelope", ns=envns)
         env.addPrefix(Namespace.xsins[0], Namespace.xsins[1])
         env.append(header)
         env.append(body)
@@ -265,56 +285,63 @@ class Binding:
     def header(self, content):
         """
         Build the B{<Body/>} for a SOAP outbound message.
+
         @param content: The header content.
         @type content: L{Element}
-        @return: the SOAP body fragment.
+        @return: The SOAP body fragment.
         @rtype: L{Element}
+
         """
-        header = Element('Header', ns=envns)
+        header = Element("Header", ns=envns)
         header.append(content)
         return header
 
     def bodycontent(self, method, args, kwargs):
         """
         Get the content for the SOAP I{body} node.
+
         @param method: A service method.
         @type method: I{service.Method}
-        @param args: method parameter values
+        @param args: method parameter values.
         @type args: list
         @param kwargs: Named (keyword) args for the method invoked.
         @type kwargs: dict
-        @return: The XML content for the <body/>
-        @rtype: [L{Element},..]
+        @return: The XML content for the <body/>.
+        @rtype: [L{Element},...]
+
         """
-        raise Exception, 'not implemented'
+        raise Exception("not implemented")
 
     def headercontent(self, method):
         """
         Get the content for the SOAP I{Header} node.
+
         @param method: A service method.
         @type method: I{service.Method}
-        @return: The XML content for the <body/>
-        @rtype: [L{Element},..]
+        @return: The XML content for the <body/>.
+        @rtype: [L{Element},...]
+
         """
-        n = 0
         content = []
         wsse = self.options().wsse
         if wsse is not None:
             content.append(wsse.xml())
         headers = self.options().soapheaders
-        if not isinstance(headers, (tuple,list,dict)):
+        if not isinstance(headers, (tuple, list, dict)):
             headers = (headers,)
-        if len(headers) == 0:
+        elif not headers:
             return content
         pts = self.headpart_types(method)
-        if isinstance(headers, (tuple,list)):
+        if isinstance(headers, (tuple, list)):
+            n = 0
             for header in headers:
                 if isinstance(header, Element):
                     content.append(deepcopy(header))
                     continue
-                if len(pts) == n: break
+                if len(pts) == n:
+                    break
                 h = self.mkheader(method, pts[n], header)
-                ns = pts[n][1].namespace('ns0')
+                ns = pts[n][1].namespace("ns0")
                 h.setPrefix(ns[0], ns[1])
                 content.append(h)
                 n += 1
@@ -324,7 +351,7 @@ class Binding:
                 if header is None:
                     continue
                 h = self.mkheader(method, pt, header)
-                ns = pt[1].namespace('ns0')
+                ns = pt[1].namespace("ns0")
                 h.setPrefix(ns[0], ns[1])
                 content.append(h)
         return content
@@ -332,118 +359,127 @@ class Binding:
     def replycontent(self, method, body):
         """
         Get the reply body content.
+
         @param method: A service method.
         @type method: I{service.Method}
         @param body: The SOAP body.
         @type body: L{Element}
         @return: The body content.
         @rtype: [L{Element},...]
+
         """
-        raise Exception, 'not implemented'
+        raise Exception("not implemented")
 
     def body(self, content):
         """
         Build the B{<Body/>} for a SOAP outbound message.
+
         @param content: The body content.
         @type content: L{Element}
         @return: The SOAP body fragment.
         @rtype: L{Element}
+
         """
-        body = Element('Body', ns=envns)
+        body = Element("Body", ns=envns)
         body.append(content)
         return body
 
     def bodypart_types(self, method, input=True):
         """
-        Get a list of I{parameter definitions} (pdef) defined for the specified
-        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
+        Get a list of I{parameter definitions} (pdefs) defined for the
+        specified method.
+
+        An input I{pdef} is a (I{name}, L{xsd.sxbase.SchemaObject}) tuple,
+        while an output I{pdef} is a L{xsd.sxbase.SchemaObject}.
+
         @param method: A service method.
         @type method: I{service.Method}
         @param input: Defines input/output message.
         @type input: boolean
         @return:  A list of parameter definitions
-        @rtype: [I{pdef},]
+        @rtype: [I{pdef},...]
+
         """
-        result = []
         if input:
             parts = method.soap.input.body.parts
         else:
             parts = method.soap.output.body.parts
-        for p in parts:
-            if p.element is not None:
-                query = ElementQuery(p.element)
-            else:
-                query = TypeQuery(p.type)
-            pt = query.execute(self.schema())
-            if pt is None:
-                raise TypeNotFound(query.ref)
-            if p.type is not None:
-                pt = PartElement(p.name, pt)
-            if input:
-                if pt.name is None:
-                    result.append((p.name, pt))
-                else:
-                    result.append((pt.name, pt))
-            else:
-                result.append(pt)
-        return result
+        return [self.__part_type(p, input) for p in parts]
 
     def headpart_types(self, method, input=True):
         """
-        Get a list of I{parameter definitions} (pdef) defined for the specified
-        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
+        Get a list of header I{parameter definitions} (pdefs) defined for the
+        specified method.
+
+        An input I{pdef} is a (I{name}, L{xsd.sxbase.SchemaObject}) tuple,
+        while an output I{pdef} is a L{xsd.sxbase.SchemaObject}.
+
         @param method: A service method.
         @type method: I{service.Method}
         @param input: Defines input/output message.
         @type input: boolean
         @return:  A list of parameter definitions
-        @rtype: [I{pdef},]
+        @rtype: [I{pdef},...]
+
         """
-        result = []
         if input:
             headers = method.soap.input.headers
         else:
             headers = method.soap.output.headers
-        for header in headers:
-            part = header.part
-            if part.element is not None:
-                query = ElementQuery(part.element)
-            else:
-                query = TypeQuery(part.type)
-            pt = query.execute(self.schema())
-            if pt is None:
-                raise TypeNotFound(query.ref)
-            if part.type is not None:
-                pt = PartElement(part.name, pt)
-            if input:
-                if pt.name is None:
-                    result.append((part.name, pt))
-                else:
-                    result.append((pt.name, pt))
-            else:
-                result.append(pt)
-        return result
+        return [self.__part_type(h.part, input) for h in headers]
 
     def returned_types(self, method):
         """
-        Get the L{xsd.sxbase.SchemaObject} returned by the I{method}.
+        Get the I{method} return value type(s).
+
         @param method: A service method.
         @type method: I{service.Method}
-        @return: The name of the type return by the method.
-        @rtype: [I{rtype},..]
+        @return: Method return value type.
+        @rtype: [L{xsd.sxbase.SchemaObject},...]
+
+        """
+        return self.bodypart_types(method, input=False)
+
+    def __part_type(self, part, input):
+        """
+        Get a I{parameter definition} (pdef) defined for a given body or header
+        message part.
+
+        An input I{pdef} is a (I{name}, L{xsd.sxbase.SchemaObject}) tuple,
+        while an output I{pdef} is a L{xsd.sxbase.SchemaObject}.
+
+        @param part: A service method input or output part.
+        @type part: I{suds.wsdl.Part}
+        @param input: Defines input/output message.
+        @type input: boolean
+        @return:  A list of parameter definitions
+        @rtype: [I{pdef},...]
+
         """
-        result = []
-        for rt in self.bodypart_types(method, input=False):
-            result.append(rt)
-        return result
+        if part.element is None:
+            query = TypeQuery(part.type)
+        else:
+            query = ElementQuery(part.element)
+        part_type = query.execute(self.schema())
+        if part_type is None:
+            raise TypeNotFound(query.ref)
+        if part.type is not None:
+            part_type = PartElement(part.name, part_type)
+        if not input:
+            return part_type
+        if part_type.name is None:
+            return part.name, part_type
+        return part_type.name, part_type
 
 
 class PartElement(SchemaElement):
     """
-    A part used to represent a message part when the part
-    references a schema type and thus assumes to be an element.
+    A part used to represent a message part when the part references a schema
+    type and thus assumes to be an element.
+
     @ivar resolved: The part type.
     @type resolved: L{suds.xsd.sxbase.SchemaObject}
+
     """
 
     def __init__(self, name, resolved):
@@ -452,8 +488,9 @@ class PartElement(SchemaElement):
         @type name: str
         @param resolved: The part type.
         @type resolved: L{suds.xsd.sxbase.SchemaObject}
+
         """
-        root = Element('element', ns=Namespace.xsdns)
+        root = Element("element", ns=Namespace.xsdns)
         SchemaElement.__init__(self, resolved.schema, root)
         self.__resolved = resolved
         self.name = name
diff --git a/suds/bindings/document.py b/suds/bindings/document.py
index 84ef566..8d36fb2 100644
--- a/suds/bindings/document.py
+++ b/suds/bindings/document.py
@@ -14,7 +14,7 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides classes for the (WS) SOAP I{document/literal} binding.
+Classes for the (WS) SOAP I{document/literal} binding.
 
 """
 
@@ -54,8 +54,6 @@ class Document(Binding):
 
     """
     def bodycontent(self, method, args, kwargs):
-        if not len(method.soap.input.body.parts):
-            return ()
         wrapped = method.soap.input.body.wrapped
         if wrapped:
             pts = self.bodypart_types(method)
@@ -97,8 +95,7 @@ class Document(Binding):
         return root
 
     def replycontent(self, method, body):
-        wrapped = method.soap.output.body.wrapped
-        if wrapped:
+        if method.soap.output.body.wrapped:
             return body[0].children
         return body.children
 
@@ -106,10 +103,12 @@ class Document(Binding):
         """
         Get the document root. For I{document/literal}, this is the name of the
         wrapper element qualified by the schema's target namespace.
+
         @param wrapper: The method name.
         @type wrapper: L{xsd.sxbase.SchemaObject}
         @return: A root element.
         @rtype: L{Element}
+
         """
         tag = wrapper[1].name
         ns = wrapper[1].namespace("ns0")
@@ -123,35 +122,19 @@ class Document(Binding):
 
         """
         if isinstance(object, (list, tuple)):
-            tags = []
-            for item in object:
-                tags.append(self.mkparam(method, pdef, item))
-            return tags
-        return Binding.mkparam(self, method, pdef, object)
+            return [self.mkparam(method, pdef, item) for item in object]
+        return super(Document, self).mkparam(method, pdef, object)
 
     def param_defs(self, method):
         """Get parameter definitions for document literal."""
         pts = self.bodypart_types(method)
-        wrapped = method.soap.input.body.wrapped
-        if not wrapped:
+        if not method.soap.input.body.wrapped:
             return pts
-        result = []
-        for p in pts:
-            for child, ancestry in p[1].resolve():
-                if not child.isattr():
-                    result.append((child.name, child, ancestry))
-        return result
+        pt = pts[0][1].resolve()
+        return [(c.name, c, a) for c, a in pt if not c.isattr()]
 
     def returned_types(self, method):
-        result = []
-        wrapped = method.soap.output.body.wrapped
-        rts = self.bodypart_types(method, input=False)
-        if wrapped:
-            for pt in rts:
-                resolved = pt.resolve(nobuiltin=True)
-                for child, ancestry in resolved:
-                    result.append(child)
-                break
-        else:
-            result += rts
-        return result
+        rts = super(Document, self).returned_types(method)
+        if not method.soap.output.body.wrapped:
+            return rts
+        return [child for child, ancestry in rts[0].resolve(nobuiltin=True)]
diff --git a/suds/bindings/rpc.py b/suds/bindings/rpc.py
index d164960..f0d5bd9 100644
--- a/suds/bindings/rpc.py
+++ b/suds/bindings/rpc.py
@@ -14,7 +14,8 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides classes for the (WS) SOAP I{rpc/literal} and I{rpc/encoded} bindings.
+Classes for the (WS) SOAP I{rpc/literal} and I{rpc/encoded} bindings.
+
 """
 
 from suds import *
@@ -24,21 +25,19 @@ from suds.bindings.binding import Binding, envns
 from suds.sax.element import Element
 
 
-encns = ('SOAP-ENC', 'http://schemas.xmlsoap.org/soap/encoding/')
+encns = ("SOAP-ENC", "http://schemas.xmlsoap.org/soap/encoding/")
+
 
 class RPC(Binding):
-    """
-    RPC/Literal binding style.
-    """
+    """RPC/Literal binding style."""
 
     def param_defs(self, method):
         return self.bodypart_types(method)
 
     def envelope(self, header, body):
-        env = Binding.envelope(self, header, body)
+        env = super(RPC, self).envelope(header, body)
         env.addPrefix(encns[0], encns[1])
-        env.set('%s:encodingStyle' % envns[0],
-                'http://schemas.xmlsoap.org/soap/encoding/')
+        env.set("%s:encodingStyle" % (envns[0],), encns[1])
         return env
 
     def bodycontent(self, method, args, kwargs):
@@ -60,24 +59,23 @@ class RPC(Binding):
 
     def method(self, method):
         """
-        Get the document root.  For I{rpc/(literal|encoded)}, this is the
-        name of the method qualified by the schema tns.
+        Get the document root. For I{rpc/(literal|encoded)}, this is the name
+        of the method qualified by the schema tns.
+
         @param method: A service method.
         @type method: I{service.Method}
         @return: A root element.
         @rtype: L{Element}
+
         """
         ns = method.soap.input.body.namespace
         if ns[0] is None:
             ns = ('ns0', ns[1])
-        method = Element(method.name, ns=ns)
-        return method
+        return Element(method.name, ns=ns)
 
 
 class Encoded(RPC):
-    """
-    RPC/Encoded (section 5)  binding style.
-    """
+    """RPC/Encoded (section 5) binding style."""
 
     def marshaller(self):
         return MxEncoded(self.schema())
@@ -85,7 +83,9 @@ class Encoded(RPC):
     def unmarshaller(self):
         """
         Get the appropriate schema based XML decoder.
+
         @return: Typed unmarshaller.
         @rtype: L{UmxTyped}
+
         """
         return UmxEncoded(self.schema())
diff --git a/suds/cache.py b/suds/cache.py
index e156d19..c0c10c1 100644
--- a/suds/cache.py
+++ b/suds/cache.py
@@ -1,88 +1,87 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains basic caching classes.
+Basic caching classes.
+
 """
 
 import suds
-from suds.transport import *
-from suds.sax.parser import Parser
-from suds.sax.element import Element
+import suds.sax.element
+import suds.sax.parser
 
-from datetime import datetime as dt
-from datetime import timedelta
+import datetime
 import os
-from tempfile import gettempdir as tmp
 try:
     import cPickle as pickle
 except Exception:
     import pickle
+import shutil
+import tempfile
 
 from logging import getLogger
 log = getLogger(__name__)
 
 
-class Cache:
-    """
-    An object object cache.
-    """
+class Cache(object):
+    """An object cache."""
 
     def get(self, id):
         """
-        Get a object from the cache by ID.
-        @param id: The object ID.
+        Get an object from the cache by id.
+
+        @param id: The object id.
         @type id: str
-        @return: The object, else None
+        @return: The object, else None.
         @rtype: any
+
         """
-        raise Exception('not-implemented')
+        raise Exception("not-implemented")
 
     def put(self, id, object):
         """
-        Put a object into the cache.
-        @param id: The object ID.
+        Put an object into the cache.
+
+        @param id: The object id.
         @type id: str
         @param object: The object to add.
         @type object: any
+
         """
-        raise Exception('not-implemented')
+        raise Exception("not-implemented")
 
     def purge(self, id):
         """
-        Purge a object from the cache by id.
-        @param id: A object ID.
+        Purge an object from the cache by id.
+
+        @param id: A object id.
         @type id: str
+
         """
-        raise Exception('not-implemented')
+        raise Exception("not-implemented")
 
     def clear(self):
-        """
-        Clear all objects from the cache.
-        """
-        raise Exception('not-implemented')
+        """Clear all objects from the cache."""
+        raise Exception("not-implemented")
 
 
 class NoCache(Cache):
-    """
-    The passthru object cache.
-    """
+    """The pass-through object cache."""
 
     def get(self, id):
-        return None
+        return
 
     def put(self, id, object):
         pass
@@ -91,211 +90,242 @@ class NoCache(Cache):
 class FileCache(Cache):
     """
     A file-based URL cache.
+
     @cvar fnprefix: The file name prefix.
-    @type fnsuffix: str
-    @ivar duration: The cached file duration which defines how
-        long the file will be cached.
-    @type duration: (unit, value)
-    @ivar location: The directory for the cached files.
+    @type fnprefix: str
+    @cvar remove_default_location_on_exit: Whether to remove the default cache
+        location on process exit (default=True).
+    @type remove_default_location_on_exit: bool
+    @ivar duration: The duration after which cached entries expire (0=never).
+    @type duration: datetime.timedelta
+    @ivar location: The cached file folder.
     @type location: str
+
     """
-    fnprefix = 'suds'
-    units = ('months', 'weeks', 'days', 'hours', 'minutes', 'seconds')
+    fnprefix = "suds"
+    __default_location = None
+    remove_default_location_on_exit = True
 
     def __init__(self, location=None, **duration):
         """
-        @param location: The directory for the cached files.
+        Initialized a new FileCache instance.
+
+        If no cache location is specified, a temporary default location will be
+        used. Such default cache location will be shared by all FileCache
+        instances with no explicitly specified location within the same
+        process. The default cache location will be removed automatically on
+        process exit unless user sets the remove_default_location_on_exit
+        FileCache class attribute to False.
+
+        @param location: The cached file folder.
         @type location: str
-        @param duration: The cached file duration which defines how
-            long the file will be cached.  A duration=0 means forever.
-            The duration may be: (months|weeks|days|hours|minutes|seconds).
-        @type duration: {unit:value}
+        @param duration: The duration after which cached entries expire
+            (default: 0=never).
+        @type duration: keyword arguments for datetime.timedelta constructor
+
         """
         if location is None:
-            location = os.path.join(tmp(), 'suds')
+            location = self.__get_default_location()
         self.location = location
-        self.duration = (None, 0)
-        self.setduration(**duration)
-        self.checkversion()
+        self.duration = datetime.timedelta(**duration)
+        self.__check_version()
+
+    def clear(self):
+        for filename in os.listdir(self.location):
+            path = os.path.join(self.location, filename)
+            if os.path.isdir(path):
+                continue
+            if filename.startswith(self.fnprefix):
+                os.remove(path)
+                log.debug("deleted: %s", path)
 
     def fnsuffix(self):
         """
-        Get the file name suffix
-        @return: The suffix
+        Get the file name suffix.
+
+        @return: The suffix.
         @rtype: str
-        """
-        return 'gcf'
 
-    def setduration(self, **duration):
-        """
-        Set the caching duration which defines how long the
-        file will be cached.
-        @param duration: The cached file duration which defines how
-            long the file will be cached.  A duration=0 means forever.
-            The duration may be: (months|weeks|days|hours|minutes|seconds).
-        @type duration: {unit:value}
         """
-        if len(duration) == 1:
-            arg = duration.items()[0]
-            if not arg[0] in self.units:
-                raise Exception('must be: %s' % str(self.units))
-            self.duration = arg
-        return self
+        return "gcf"
 
-    def setlocation(self, location):
-        """
-        Set the location (directory) for the cached files.
-        @param location: The directory for the cached files.
-        @type location: str
-        """
-        self.location = location
+    def get(self, id):
+        try:
+            f = self._getf(id)
+            try:
+                return f.read()
+            finally:
+                f.close()
+        except Exception:
+            pass
 
-    def mktmp(self):
-        """
-        Make the I{location} directory if it doesn't already exits.
-        """
+    def purge(self, id):
+        filename = self.__filename(id)
         try:
-            if not os.path.isdir(self.location):
-                os.makedirs(self.location)
+            os.remove(filename)
         except Exception:
-            log.debug(self.location, exc_info=1)
-        return self
+            pass
 
-    def put(self, id, bfr):
+    def put(self, id, data):
         try:
-            fn = self.__fn(id)
-            f = self.open(fn, 'wb')
+            filename = self.__filename(id)
+            f = self.__open(filename, "wb")
             try:
-                f.write(bfr)
+                f.write(data)
             finally:
                 f.close()
-            return bfr
+            return data
         except Exception:
             log.debug(id, exc_info=1)
-            return bfr
+            return data
 
-    def get(self, id):
+    def _getf(self, id):
+        """Open a cached file with the given id for reading."""
         try:
-            f = self.getf(id)
-            try:
-                return f.read()
-            finally:
-                f.close()
+            filename = self.__filename(id)
+            self.__remove_if_expired(filename)
+            return self.__open(filename, "rb")
         except Exception:
             pass
 
-    def getf(self, id):
+    def __check_version(self):
+        path = os.path.join(self.location, "version")
         try:
-            fn = self.__fn(id)
-            self.validate(fn)
-            return self.open(fn, 'rb')
+            f = self.__open(path)
+            try:
+                version = f.read()
+            finally:
+                f.close()
+            if version != suds.__version__:
+                raise Exception()
         except Exception:
-            pass
+            self.clear()
+            f = self.__open(path, "w")
+            try:
+                f.write(suds.__version__)
+            finally:
+                f.close()
 
-    def validate(self, fn):
-        """
-        Validate that the file has not expired based on the I{duration}.
-        @param fn: The file name.
-        @type fn: str
+    def __filename(self, id):
+        """Return the cache file name for an entry with a given id."""
+        suffix = self.fnsuffix()
+        filename = "%s-%s.%s" % (self.fnprefix, id, suffix)
+        return os.path.join(self.location, filename)
+
+    @staticmethod
+    def __get_default_location():
         """
-        if self.duration[1] < 1:
-            return
-        created = dt.fromtimestamp(os.path.getctime(fn))
-        d = {self.duration[0]:self.duration[1]}
-        expired = created + timedelta(**d)
-        if expired < dt.now():
-            log.debug('%s expired, deleted', fn)
-            os.remove(fn)
+        Returns the current process's default cache location folder.
 
-    def clear(self):
-        for fn in os.listdir(self.location):
-            path = os.path.join(self.location, fn)
-            if os.path.isdir(path):
-                continue
-            if fn.startswith(self.fnprefix):
-                os.remove(path)
-                log.debug('deleted: %s', path)
+        The folder is determined lazily on first call.
 
-    def purge(self, id):
-        fn = self.__fn(id)
+        """
+        if not FileCache.__default_location:
+            tmp = tempfile.mkdtemp("suds-default-cache")
+            FileCache.__default_location = tmp
+            import atexit
+            atexit.register(FileCache.__remove_default_location)
+        return FileCache.__default_location
+
+    def __mktmp(self):
+        """Create the I{location} folder if it does not already exist."""
         try:
-            os.remove(fn)
+            if not os.path.isdir(self.location):
+                os.makedirs(self.location)
         except Exception:
-            pass
+            log.debug(self.location, exc_info=1)
+        return self
+
+    def __open(self, filename, *args):
+        """Open cache file making sure the I{location} folder is created."""
+        self.__mktmp()
+        return open(filename, *args)
 
-    def open(self, fn, *args):
+    @staticmethod
+    def __remove_default_location():
         """
-        Open the cache file making sure the directory is created.
+        Removes the default cache location folder.
+
+        This removal may be disabled by setting the
+        remove_default_location_on_exit FileCache class attribute to False.
+
+        """
+        if FileCache.remove_default_location_on_exit:
+            # We must not load shutil here on-demand as under some
+            # circumstances this may cause the shutil.rmtree() operation to
+            # fail due to not having some internal module loaded. E.g. this
+            # happens if you run the project's test suite using the setup.py
+            # test command on Python 2.4.x.
+            shutil.rmtree(FileCache.__default_location, ignore_errors=True)
+
+    def __remove_if_expired(self, filename):
         """
-        self.mktmp()
-        return open(fn, *args)
+        Remove a cached file entry if it expired.
 
-    def checkversion(self):
-        path = os.path.join(self.location, 'version')
-        try:
-            f = self.open(path)
-            version = f.read()
-            f.close()
-            if version != suds.__version__:
-                raise Exception()
-        except Exception:
-            self.clear()
-            f = self.open(path, 'w')
-            f.write(suds.__version__)
-            f.close()
+        @param filename: The file name.
+        @type filename: str
 
-    def __fn(self, id):
-        name = id
-        suffix = self.fnsuffix()
-        fn = '%s-%s.%s' % (self.fnprefix, name, suffix)
-        return os.path.join(self.location, fn)
+        """
+        if not self.duration:
+            return
+        created = datetime.datetime.fromtimestamp(os.path.getctime(filename))
+        expired = created + self.duration
+        if expired < datetime.datetime.now():
+            os.remove(filename)
+            log.debug("%s expired, deleted", filename)
 
 
 class DocumentCache(FileCache):
-    """
-    Provides xml document caching.
-    """
+    """XML document file cache."""
 
     def fnsuffix(self):
-        return 'xml'
+        return "xml"
 
     def get(self, id):
+        fp = None
         try:
-            fp = self.getf(id)
+            fp = self._getf(id)
             if fp is None:
                 return None
-            p = Parser()
+            p = suds.sax.parser.Parser()
             return p.parse(fp)
         except Exception:
+            if fp is not None:
+                fp.close()
             self.purge(id)
 
     def put(self, id, object):
-        if isinstance(object, Element):
-            FileCache.put(self, id, suds.byte_str(str(object)))
+        if isinstance(object,
+                (suds.sax.document.Document, suds.sax.element.Element)):
+            super(DocumentCache, self).put(id, suds.byte_str(str(object)))
         return object
 
 
 class ObjectCache(FileCache):
     """
-    Provides pickled object caching.
+    Pickled object file cache.
+
     @cvar protocol: The pickling protocol.
     @type protocol: int
+
     """
     protocol = 2
 
     def fnsuffix(self):
-        return 'px'
+        return "px"
 
     def get(self, id):
+        fp = None
         try:
-            fp = self.getf(id)
-            if fp is None:
-                return None
-            return pickle.load(fp)
+            fp = self._getf(id)
+            if fp is not None:
+                return pickle.load(fp)
         except Exception:
+            if fp is not None:
+                fp.close()
             self.purge(id)
 
     def put(self, id, object):
-        bfr = pickle.dumps(object, self.protocol)
-        FileCache.put(self, id, bfr)
+        data = pickle.dumps(object, self.protocol)
+        super(ObjectCache, self).put(id, data)
         return object
diff --git a/suds/client.py b/suds/client.py
index d7fda60..feec47a 100644
--- a/suds/client.py
+++ b/suds/client.py
@@ -1,29 +1,28 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{2nd generation} service proxy provides access to web services.
-See I{README.txt}
+Service proxy implementation providing access to web services.
+
 """
 
 import suds
 from suds import *
 import suds.bindings.binding
 from suds.builder import Builder
-from suds.cache import ObjectCache
+import suds.cache
 import suds.metrics as metrics
 from suds.options import Options
 from suds.plugin import PluginContainer
@@ -31,10 +30,10 @@ from suds.properties import Unskin
 from suds.reader import DefinitionsReader
 from suds.resolver import PathResolver
 from suds.sax.document import Document
-from suds.sax.parser import Parser
+import suds.sax.parser
 from suds.servicedefinition import ServiceDefinition
-from suds.transport import TransportError, Request
-from suds.transport.https import HttpAuthenticated
+import suds.transport
+import suds.transport.https
 from suds.umx.basic import Basic as UmxBasic
 from suds.wsdl import Definitions
 import sudsobject
@@ -42,7 +41,6 @@ import sudsobject
 from cookielib import CookieJar
 from copy import deepcopy
 import httplib
-from urlparse import urlparse
 
 from logging import getLogger
 log = getLogger(__name__)
@@ -50,8 +48,8 @@ log = getLogger(__name__)
 
 class Client(UnicodeMixin):
     """
-    A lightweight web services client.
-    I{(2nd generation)} API.
+    A lightweight web service client.
+
     @ivar wsdl: The WSDL object.
     @type wsdl:L{Definitions}
     @ivar service: The service proxy used to invoke operations.
@@ -60,18 +58,21 @@ class Client(UnicodeMixin):
     @type factory: L{Factory}
     @ivar sd: The service definition
     @type sd: L{ServiceDefinition}
-    @ivar messages: The last sent/received messages.
-    @type messages: str[2]
+
     """
+
     @classmethod
     def items(cls, sobject):
         """
-        Extract the I{items} from a suds object much like the
-        items() method works on I{dict}.
+        Extract I{items} from a suds object.
+
+        Much like the items() method works on I{dict}.
+
         @param sobject: A suds object
         @type sobject: L{Object}
         @return: A list of items contained in I{sobject}.
         @rtype: [(key, value),...]
+
         """
         return sudsobject.items(sobject)
 
@@ -79,11 +80,12 @@ class Client(UnicodeMixin):
     def dict(cls, sobject):
         """
         Convert a sudsobject into a dictionary.
+
         @param sobject: A suds object
         @type sobject: L{Object}
-        @return: A python dictionary containing the
-            items contained in I{sobject}.
+        @return: A dictionary of items contained in I{sobject}.
         @rtype: dict
+
         """
         return sudsobject.asdict(sobject)
 
@@ -91,10 +93,12 @@ class Client(UnicodeMixin):
     def metadata(cls, sobject):
         """
         Extract the metadata from a suds object.
+
         @param sobject: A suds object
         @type sobject: L{Object}
         @return: The object's metadata
         @rtype: L{sudsobject.Metadata}
+
         """
         return sobject.__metadata__
 
@@ -104,12 +108,13 @@ class Client(UnicodeMixin):
         @type url: str
         @param kwargs: keyword arguments.
         @see: L{Options}
+
         """
         options = Options()
-        options.transport = HttpAuthenticated()
+        options.transport = suds.transport.https.HttpAuthenticated()
         self.options = options
         if "cache" not in kwargs:
-            kwargs["cache"] = ObjectCache(days=1)
+            kwargs["cache"] = suds.cache.ObjectCache(days=1)
         self.set_options(**kwargs)
         reader = DefinitionsReader(options, Definitions)
         self.wsdl = reader.open(url)
@@ -121,13 +126,14 @@ class Client(UnicodeMixin):
         for s in self.wsdl.services:
             sd = ServiceDefinition(self.wsdl, s)
             self.sd.append(sd)
-        self.messages = dict(tx=None, rx=None)
 
     def set_options(self, **kwargs):
         """
         Set options.
+
         @param kwargs: keyword arguments.
         @see: L{Options}
+
         """
         p = Unskin(self.options)
         p.update(kwargs)
@@ -135,13 +141,16 @@ class Client(UnicodeMixin):
     def add_prefix(self, prefix, uri):
         """
         Add I{static} mapping of an XML namespace prefix to a namespace.
-        This is useful for cases when a wsdl and referenced schemas make heavy
-        use of namespaces and those namespaces are subject to change.
+
+        Useful for cases when a WSDL and referenced XSD schemas make heavy use
+        of namespaces and those namespaces are subject to change.
+
         @param prefix: An XML namespace prefix.
         @type prefix: str
         @param uri: An XML namespace URI.
         @type uri: str
-        @raise Exception: when prefix is already mapped.
+        @raise Exception: prefix already mapped.
+
         """
         root = self.wsdl.root
         mapped = root.resolvePrefix(prefix, None)
@@ -154,10 +163,13 @@ class Client(UnicodeMixin):
     def clone(self):
         """
         Get a shallow clone of this object.
-        The clone only shares the WSDL.  All other attributes are
-        unique to the cloned object including options.
+
+        The clone only shares the WSDL. All other attributes are unique to the
+        cloned object including options.
+
         @return: A shallow clone.
         @rtype: L{Client}
+
         """
         class Uninitialized(Client):
             def __init__(self):
@@ -171,33 +183,35 @@ class Client(UnicodeMixin):
         clone.factory = self.factory
         clone.service = ServiceSelector(clone, self.wsdl.services)
         clone.sd = self.sd
-        clone.messages = dict(tx=None, rx=None)
         return clone
 
     def __unicode__(self):
-        s = ['\n']
-        s.append('Suds ( https://fedorahosted.org/suds/ )')
-        s.append('  version: %s' % suds.__version__)
-        if ( suds.__build__ ):
-            s.append('  build: %s' % suds.__build__)
+        s = ["\n"]
+        s.append("Suds ( https://fedorahosted.org/suds/ )")
+        s.append("  version: %s" % (suds.__version__,))
+        if suds.__build__:
+            s.append("  build: %s" % (suds.__build__,))
         for sd in self.sd:
-            s.append('\n\n%s' % unicode(sd))
-        return ''.join(s)
+            s.append("\n\n%s" % (unicode(sd),))
+        return "".join(s)
 
 
 class Factory:
     """
-    A factory for instantiating types defined in the wsdl
+    A factory for instantiating types defined in the WSDL.
+
     @ivar resolver: A schema type resolver.
     @type resolver: L{PathResolver}
     @ivar builder: A schema object builder.
     @type builder: L{Builder}
+
     """
 
     def __init__(self, wsdl):
         """
         @param wsdl: A schema object.
         @type wsdl: L{wsdl.Definitions}
+
         """
         self.wsdl = wsdl
         self.resolver = PathResolver(wsdl)
@@ -205,11 +219,13 @@ class Factory:
 
     def create(self, name):
         """
-        create a WSDL type by name
+        Create a WSDL type by name.
+
         @param name: The name of a type defined in the WSDL.
         @type name: str
         @return: The requested object.
         @rtype: L{Object}
+
         """
         timer = metrics.Timer()
         timer.start()
@@ -227,14 +243,16 @@ class Factory:
                 log.error("create '%s' failed", name, exc_info=True)
                 raise BuildError(name, e)
         timer.stop()
-        metrics.log.debug('%s created: %s', name, timer)
+        metrics.log.debug("%s created: %s", name, timer)
         return result
 
     def separator(self, ps):
         """
         Set the path separator.
+
         @param ps: The new path separator.
         @type ps: char
+
         """
         self.resolver = PathResolver(self.wsdl, ps)
 
@@ -242,38 +260,44 @@ class Factory:
 class ServiceSelector:
     """
     The B{service} selector is used to select a web service.
-    In most cases, the wsdl only defines (1) service in which access
-    by subscript is passed through to a L{PortSelector}.  This is also the
-    behavior when a I{default} service has been specified.  In cases
-    where multiple services have been defined and no default has been
-    specified, the service is found by name (or index) and a L{PortSelector}
-    for the service is returned.  In all cases, attribute access is
-    forwarded to the L{PortSelector} for either the I{first} service or the
-    I{default} service (when specified).
+
+    Most WSDLs only define a single service in which case access by subscript
+    is passed through to a L{PortSelector}. This is also the behavior when a
+    I{default} service has been specified. In cases where multiple services
+    have been defined and no default has been specified, the service is found
+    by name (or index) and a L{PortSelector} for the service is returned. In
+    all cases, attribute access is forwarded to the L{PortSelector} for either
+    the I{first} service or the I{default} service (when specified).
+
     @ivar __client: A suds client.
     @type __client: L{Client}
-    @ivar __services: A list of I{wsdl} services.
+    @ivar __services: A list of I{WSDL} services.
     @type __services: list
+
     """
     def __init__(self, client, services):
         """
         @param client: A suds client.
         @type client: L{Client}
-        @param services: A list of I{wsdl} services.
+        @param services: A list of I{WSDL} services.
         @type services: list
+
         """
         self.__client = client
         self.__services = services
 
     def __getattr__(self, name):
         """
-        Request to access an attribute is forwarded to the
-        L{PortSelector} for either the I{first} service or the
-        I{default} service (when specified).
-        @param name: The name of a method.
+        Attribute access is forwarded to the L{PortSelector}.
+
+        Uses the I{default} service if specified or the I{first} service
+        otherwise.
+
+        @param name: Method name.
         @type name: str
         @return: A L{PortSelector}.
         @rtype: L{PortSelector}.
+
         """
         default = self.__ds()
         if default is None:
@@ -284,14 +308,16 @@ class ServiceSelector:
 
     def __getitem__(self, name):
         """
-        Provides selection of the I{service} by name (string) or
-        index (integer).  In cases where only (1) service is defined
-        or a I{default} has been specified, the request is forwarded
-        to the L{PortSelector}.
+        Provides I{service} selection by name (string) or index (integer).
+
+        In cases where only a single service is defined or a I{default} has
+        been specified, the request is forwarded to the L{PortSelector}.
+
         @param name: The name (or index) of a service.
-        @type name: (int|str)
+        @type name: int|str
         @return: A L{PortSelector} for the specified service.
         @rtype: L{PortSelector}.
+
         """
         if len(self.__services) == 1:
             port = self.__find(0)
@@ -305,20 +331,22 @@ class ServiceSelector:
     def __find(self, name):
         """
         Find a I{service} by name (string) or index (integer).
+
         @param name: The name (or index) of a service.
-        @type name: (int|str)
+        @type name: int|str
         @return: A L{PortSelector} for the found service.
         @rtype: L{PortSelector}.
+
         """
         service = None
-        if not len(self.__services):
-            raise Exception, 'No services defined'
+        if not self.__services:
+            raise Exception, "No services defined"
         if isinstance(name, int):
             try:
                 service = self.__services[name]
                 name = service.name
             except IndexError:
-                raise ServiceNotFound, 'at [%d]' % name
+                raise ServiceNotFound, "at [%d]" % (name,)
         else:
             for s in self.__services:
                 if name == s.name:
@@ -331,30 +359,33 @@ class ServiceSelector:
     def __ds(self):
         """
         Get the I{default} service if defined in the I{options}.
+
         @return: A L{PortSelector} for the I{default} service.
         @rtype: L{PortSelector}.
+
         """
         ds = self.__client.options.service
-        if ds is None:
-            return None
-        else:
+        if ds is not None:
             return self.__find(ds)
 
 
 class PortSelector:
     """
     The B{port} selector is used to select a I{web service} B{port}.
+
     In cases where multiple ports have been defined and no default has been
-    specified, the port is found by name (or index) and a L{MethodSelector}
-    for the port is returned.  In all cases, attribute access is
-    forwarded to the L{MethodSelector} for either the I{first} port or the
-    I{default} port (when specified).
+    specified, the port is found by name (or index) and a L{MethodSelector} for
+    the port is returned. In all cases, attribute access is forwarded to the
+    L{MethodSelector} for either the I{first} port or the I{default} port (when
+    specified).
+
     @ivar __client: A suds client.
     @type __client: L{Client}
     @ivar __ports: A list of I{service} ports.
     @type __ports: list
     @ivar __qn: The I{qualified} name of the port (used for logging).
     @type __qn: str
+
     """
     def __init__(self, client, ports, qn):
         """
@@ -364,6 +395,7 @@ class PortSelector:
         @type ports: list
         @param qn: The name of the service.
         @type qn: str
+
         """
         self.__client = client
         self.__ports = ports
@@ -371,13 +403,15 @@ class PortSelector:
 
     def __getattr__(self, name):
         """
-        Request to access an attribute is forwarded to the
-        L{MethodSelector} for either the I{first} port or the
-        I{default} port (when specified).
+        Attribute access is forwarded to the L{MethodSelector}.
+
+        Uses the I{default} port when specified or the I{first} port otherwise.
+
         @param name: The name of a method.
         @type name: str
         @return: A L{MethodSelector}.
         @rtype: L{MethodSelector}.
+
         """
         default = self.__dp()
         if default is None:
@@ -388,71 +422,76 @@ class PortSelector:
 
     def __getitem__(self, name):
         """
-        Provides selection of the I{port} by name (string) or
-        index (integer).  In cases where only (1) port is defined
-        or a I{default} has been specified, the request is forwarded
-        to the L{MethodSelector}.
+        Provides I{port} selection by name (string) or index (integer).
+
+        In cases where only a single port is defined or a I{default} has been
+        specified, the request is forwarded to the L{MethodSelector}.
+
         @param name: The name (or index) of a port.
-        @type name: (int|str)
+        @type name: int|str
         @return: A L{MethodSelector} for the specified port.
         @rtype: L{MethodSelector}.
+
         """
         default = self.__dp()
         if default is None:
             return self.__find(name)
-        else:
-            return default
+        return default
 
     def __find(self, name):
         """
         Find a I{port} by name (string) or index (integer).
+
         @param name: The name (or index) of a port.
-        @type name: (int|str)
+        @type name: int|str
         @return: A L{MethodSelector} for the found port.
         @rtype: L{MethodSelector}.
+
         """
         port = None
-        if not len(self.__ports):
-            raise Exception, 'No ports defined: %s' % self.__qn
+        if not self.__ports:
+            raise Exception, "No ports defined: %s" % (self.__qn,)
         if isinstance(name, int):
-            qn = '%s[%d]' % (self.__qn, name)
+            qn = "%s[%d]" % (self.__qn, name)
             try:
                 port = self.__ports[name]
             except IndexError:
                 raise PortNotFound, qn
         else:
-            qn = '.'.join((self.__qn, name))
+            qn = ".".join((self.__qn, name))
             for p in self.__ports:
                 if name == p.name:
                     port = p
                     break
         if port is None:
             raise PortNotFound, qn
-        qn = '.'.join((self.__qn, port.name))
+        qn = ".".join((self.__qn, port.name))
         return MethodSelector(self.__client, port.methods, qn)
 
     def __dp(self):
         """
         Get the I{default} port if defined in the I{options}.
+
         @return: A L{MethodSelector} for the I{default} port.
         @rtype: L{MethodSelector}.
+
         """
         dp = self.__client.options.port
-        if dp is None:
-            return None
-        else:
+        if dp is not None:
             return self.__find(dp)
 
 
 class MethodSelector:
     """
     The B{method} selector is used to select a B{method} by name.
+
     @ivar __client: A suds client.
     @type __client: L{Client}
     @ivar __methods: A dictionary of methods.
     @type __methods: dict
     @ivar __qn: The I{qualified} name of the method (used for logging).
     @type __qn: str
+
     """
     def __init__(self, client, methods, qn):
         """
@@ -462,6 +501,7 @@ class MethodSelector:
         @type methods: dict
         @param qn: The I{qualified} name of the port.
         @type qn: str
+
         """
         self.__client = client
         self.__methods = methods
@@ -470,24 +510,28 @@ class MethodSelector:
     def __getattr__(self, name):
         """
         Get a method by name and return it in an I{execution wrapper}.
+
         @param name: The name of a method.
         @type name: str
         @return: An I{execution wrapper} for the specified method name.
         @rtype: L{Method}
+
         """
         return self[name]
 
     def __getitem__(self, name):
         """
         Get a method by name and return it in an I{execution wrapper}.
+
         @param name: The name of a method.
         @type name: str
         @return: An I{execution wrapper} for the specified method name.
         @rtype: L{Method}
+
         """
         m = self.__methods.get(name)
         if m is None:
-            qn = '.'.join((self.__qn, name))
+            qn = ".".join((self.__qn, name))
             raise MethodNotFound, qn
         return Method(self.__client, m)
 
@@ -495,10 +539,12 @@ class MethodSelector:
 class Method:
     """
     The I{method} (namespace) object.
+
     @ivar client: A client object.
     @type client: L{Client}
-    @ivar method: A I{wsdl} method.
-    @type I{wsdl} Method.
+    @ivar method: A I{WSDL} method.
+    @type I{raw} Method.
+
     """
 
     def __init__(self, client, method):
@@ -507,14 +553,13 @@ class Method:
         @type client: L{Client}
         @param method: A I{raw} method.
         @type I{raw} Method.
+
         """
         self.client = client
         self.method = method
 
     def __call__(self, *args, **kwargs):
-        """
-        Invoke the method.
-        """
+        """Invoke the method."""
         clientclass = self.clientclass(kwargs)
         client = clientclass(self.client, self.method)
         try:
@@ -522,22 +567,80 @@ class Method:
         except WebFault, e:
             if self.faults():
                 raise
-            return (httplib.INTERNAL_SERVER_ERROR, e)
+            return httplib.INTERNAL_SERVER_ERROR, e
 
     def faults(self):
-        """ get faults option """
+        """Get faults option."""
         return self.client.options.faults
 
     def clientclass(self, kwargs):
-        """ get soap client class """
-        if SimClient.simulation(kwargs):
-            return SimClient
-        return SoapClient
+        """Get SOAP client class."""
+        if _SimClient.simulation(kwargs):
+            return _SimClient
+        return _SoapClient
+
+
+class RequestContext:
+    """
+    A request context.
+
+    Returned by a suds Client when invoking a web service operation with the
+    ``nosend`` enabled. Allows the caller to take care of sending the request
+    himself and return back the reply data for further processing.
 
+    @ivar envelope: The SOAP request envelope.
+    @type envelope: I{bytes}
 
-class SoapClient:
     """
-    A lightweight soap based web client B{**not intended for external use}
+
+    def __init__(self, process_reply, envelope):
+        """
+        @param process_reply: A callback for processing a user defined reply.
+        @type process_reply: I{callable}
+        @param envelope: The SOAP request envelope.
+        @type envelope: I{bytes}
+
+        """
+        self.__process_reply = process_reply
+        self.envelope = envelope
+
+    def process_reply(self, reply, status=None, description=None):
+        """
+        Re-entry for processing a successful reply.
+
+        Depending on how the ``retxml`` option is set, may return the SOAP
+        reply XML or process it and return the Python object representing the
+        returned value.
+
+        @param reply: The SOAP reply envelope.
+        @type reply: I{bytes}
+        @param status: The HTTP status code.
+        @type status: int
+        @param description: Additional status description.
+        @type description: I{bytes}
+        @return: The invoked web service operation return value.
+        @rtype: I{builtin}|I{subclass of} L{Object}|I{bytes}|I{None}
+
+        """
+        return self.__process_reply(reply, status, description)
+
+
+class _SoapClient:
+    """
+    An internal lightweight SOAP based web service operation client.
+
+    Each instance is constructed for specific web service operation and knows
+    how to:
+      - Construct a SOAP request for it.
+      - Transport a SOAP request for it using a configured transport.
+      - Receive a SOAP reply using a configured transport.
+      - Process the received SOAP reply.
+
+    Depending on the given suds options, may do all the tasks listed above or
+    may stop the process at an earlier point and return some intermediate
+    result, e.g. the constructed SOAP request or the raw received SOAP reply.
+    See the invoke() method for more detailed information.
+
     @ivar service: The target method.
     @type service: L{Service}
     @ivar method: A target method.
@@ -546,6 +649,7 @@ class SoapClient:
     @type options: dict
     @ivar cookiejar: A cookie jar.
     @type cookiejar: libcookie.CookieJar
+
     """
 
     def __init__(self, client, method):
@@ -554,6 +658,7 @@ class SoapClient:
         @type client: L{Client}
         @param method: A target method.
         @type method: L{Method}
+
         """
         self.client = client
         self.method = method
@@ -562,271 +667,281 @@ class SoapClient:
 
     def invoke(self, args, kwargs):
         """
-        Send the required soap message to invoke the specified method
+        Invoke a specified web service method.
+
+        Depending on how the ``nosend`` & ``retxml`` options are set, may do
+        one of the following:
+          * Return a constructed web service operation SOAP request without
+            sending it to the web service.
+          * Invoke the web service operation and return its SOAP reply XML.
+          * Invoke the web service operation, process its results and return
+            the Python object representing the returned value.
+
+        When returning a SOAP request, the request is wrapped inside a
+        RequestContext object allowing the user to acquire a corresponding SOAP
+        reply himself and then pass it back to suds for further processing.
+
+        Constructed request data is automatically processed using registered
+        plugins and serialized into a byte-string. Exact request XML formatting
+        may be affected by the ``prettyxml`` suds option.
+
         @param args: A list of args for the method invoked.
-        @type args: list
+        @type args: list|tuple
         @param kwargs: Named (keyword) args for the method invoked.
         @type kwargs: dict
-        @return: The result of the method invocation.
-        @rtype: I{builtin}|I{subclass of} L{Object}
+        @return: SOAP request, SOAP reply or a web service return value.
+        @rtype: L{RequestContext}|I{builtin}|I{subclass of} L{Object}|I{bytes}|
+            I{None}
+
         """
         timer = metrics.Timer()
         timer.start()
         binding = self.method.binding.input
         soapenv = binding.get_message(self.method, args, kwargs)
         timer.stop()
-        metrics.log.debug("message for '%s' created: %s", self.method.name,
-            timer)
+        method_name = self.method.name
+        metrics.log.debug("message for '%s' created: %s", method_name, timer)
         timer.start()
         result = self.send(soapenv)
         timer.stop()
-        metrics.log.debug("method '%s' invoked: %s", self.method.name, timer)
+        metrics.log.debug("method '%s' invoked: %s", method_name, timer)
         return result
 
     def send(self, soapenv):
         """
-        Send soap message.
-        @param soapenv: A soap envelope to send.
+        Send SOAP message.
+
+        Depending on how the ``nosend`` & ``retxml`` options are set, may do
+        one of the following:
+          * Return a constructed web service operation request without sending
+            it to the web service.
+          * Invoke the web service operation and return its SOAP reply XML.
+          * Invoke the web service operation, process its results and return
+            the Python object representing the returned value.
+
+        @param soapenv: A SOAP envelope to send.
         @type soapenv: L{Document}
-        @return: The reply to the sent message.
-        @rtype: I{builtin} or I{subclass of} L{Object}
+        @return: SOAP request, SOAP reply or a web service return value.
+        @rtype: L{RequestContext}|I{builtin}|I{subclass of} L{Object}|I{bytes}|
+            I{None}
+
         """
-        location = self.location()
-        log.debug('sending to (%s)\nmessage:\n%s', location, soapenv)
-        original_soapenv = soapenv
+        location = self.__location()
+        log.debug("sending to (%s)\nmessage:\n%s", location, soapenv)
         plugins = PluginContainer(self.options.plugins)
         plugins.message.marshalled(envelope=soapenv.root())
         if self.options.prettyxml:
             soapenv = soapenv.str()
         else:
             soapenv = soapenv.plain()
-        soapenv = soapenv.encode('utf-8')
+        soapenv = soapenv.encode("utf-8")
         ctx = plugins.message.sending(envelope=soapenv)
         soapenv = ctx.envelope
         if self.options.nosend:
-            return RequestContext(self, soapenv, original_soapenv)
-        request = Request(location, soapenv)
-        request.headers = self.headers()
+            return RequestContext(self.process_reply, soapenv)
+        request = suds.transport.Request(location, soapenv)
+        request.headers = self.__headers()
         try:
             timer = metrics.Timer()
             timer.start()
             reply = self.options.transport.send(request)
             timer.stop()
-            metrics.log.debug('waited %s on server reply', timer)
-        except TransportError, e:
-            content = e.fp and e.fp.read() or ''
-            return self.process_reply(reply=content, status=e.httpcode,
-                description=tostr(e), original_soapenv=original_soapenv)
-        return self.process_reply(reply=reply.message,
-            original_soapenv=original_soapenv)
-
-    def process_reply(self, reply, status=None, description=None,
-        original_soapenv=None):
+            metrics.log.debug("waited %s on server reply", timer)
+        except suds.transport.TransportError, e:
+            content = e.fp and e.fp.read() or ""
+            return self.process_reply(content, e.httpcode, tostr(e))
+        return self.process_reply(reply.message, None, None)
+
+    def process_reply(self, reply, status, description):
+        """
+        Process a web service operation SOAP reply.
+
+        Depending on how the ``retxml`` option is set, may return the SOAP
+        reply XML or process it and return the Python object representing the
+        returned value.
+
+        @param reply: The SOAP reply envelope.
+        @type reply: I{bytes}
+        @param status: The HTTP status code (None indicates httplib.OK).
+        @type status: int|I{None}
+        @param description: Additional status description.
+        @type description: str
+        @return: The invoked web service operation return value.
+        @rtype: I{builtin}|I{subclass of} L{Object}|I{bytes}|I{None}
+
+        """
         if status is None:
             status = httplib.OK
+        debug_message = "Reply HTTP status - %d" % (status,)
         if status in (httplib.ACCEPTED, httplib.NO_CONTENT):
+            log.debug(debug_message)
             return
-        failed = True
-        try:
-            if status == httplib.OK:
-                log.debug('HTTP succeeded:\n%s', reply)
-            else:
-                log.debug('HTTP failed - %d - %s:\n%s', status, description,
-                    reply)
-
-            # (todo)
-            #   Consider whether and how to allow plugins to handle error,
-            # httplib.ACCEPTED & httplib.NO_CONTENT replies as well as
-            # successful ones.
-            #                                 (todo) (27.03.2013.) (Jurko)
-            plugins = PluginContainer(self.options.plugins)
-            ctx = plugins.message.received(reply=reply)
-            reply = ctx.reply
-
-            # SOAP standard states that SOAP errors must be accompanied by HTTP
-            # status code 500 - internal server error:
-            #
-            # From SOAP 1.1 Specification:
-            #   In case of a SOAP error while processing the request, the SOAP
-            # HTTP server MUST issue an HTTP 500 "Internal Server Error"
-            # response and include a SOAP message in the response containing a
-            # SOAP Fault element (see section 4.4) indicating the SOAP
-            # processing error.
-            #
-            # From WS-I Basic profile:
-            #   An INSTANCE MUST use a "500 Internal Server Error" HTTP status
-            # code if the response message is a SOAP Fault.
-            replyroot = None
-            if status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
-                replyroot = _parse(reply)
-                plugins.message.parsed(reply=replyroot)
-                fault = self.get_fault(replyroot)
-                if fault:
-                    if status != httplib.INTERNAL_SERVER_ERROR:
-                        log.warn("Web service reported a SOAP processing "
-                            "fault using an unexpected HTTP status code %d. "
-                            "Reporting as an internal server error.", status)
-                    if self.options.faults:
-                        raise WebFault(fault, replyroot)
-                    return (httplib.INTERNAL_SERVER_ERROR, fault)
-            if status != httplib.OK:
+        #TODO: Consider whether and how to allow plugins to handle error,
+        # httplib.ACCEPTED & httplib.NO_CONTENT replies as well as successful
+        # ones.
+        if status == httplib.OK:
+            log.debug("%s\n%s", debug_message, reply)
+        else:
+            log.debug("%s - %s\n%s", debug_message, description, reply)
+
+        plugins = PluginContainer(self.options.plugins)
+        ctx = plugins.message.received(reply=reply)
+        reply = ctx.reply
+
+        # SOAP standard states that SOAP errors must be accompanied by HTTP
+        # status code 500 - internal server error:
+        #
+        # From SOAP 1.1 specification:
+        #   In case of a SOAP error while processing the request, the SOAP HTTP
+        # server MUST issue an HTTP 500 "Internal Server Error" response and
+        # include a SOAP message in the response containing a SOAP Fault
+        # element (see section 4.4) indicating the SOAP processing error.
+        #
+        # From WS-I Basic profile:
+        #   An INSTANCE MUST use a "500 Internal Server Error" HTTP status code
+        # if the response message is a SOAP Fault.
+        replyroot = None
+        if status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+            replyroot = _parse(reply)
+            plugins.message.parsed(reply=replyroot)
+            fault = self.__get_fault(replyroot)
+            if fault:
+                if status != httplib.INTERNAL_SERVER_ERROR:
+                    log.warn("Web service reported a SOAP processing fault "
+                        "using an unexpected HTTP status code %d. Reporting "
+                        "as an internal server error.", status)
                 if self.options.faults:
-                    # (todo)
-                    #   Use a more specific exception class here.
-                    #                         (27.03.2013.) (Jurko)
-                    raise Exception((status, description))
-                return (status, description)
-
-            if self.options.retxml:
-                failed = False
-                return reply
-
-            result = replyroot and self.method.binding.output.get_reply(
-                self.method, replyroot)
-            ctx = plugins.message.unmarshalled(reply=result)
-            result = ctx.reply
-            failed = False
+                    raise WebFault(fault, replyroot)
+                return httplib.INTERNAL_SERVER_ERROR, fault
+        if status != httplib.OK:
             if self.options.faults:
-                return result
-            return (httplib.OK, result)
-        finally:
-            if failed and original_soapenv:
-                log.error(original_soapenv)
+                #TODO: Use a more specific exception class here.
+                raise Exception((status, description))
+            return status, description
 
-    def get_fault(self, replyroot):
-        """Extract fault information from the specified SOAP reply.
+        if self.options.retxml:
+            return reply
 
-          Returns an I{unmarshalled} fault L{Object} or None in case the given
-        XML document does not contain the SOAP <Fault> element.
+        result = replyroot and self.method.binding.output.get_reply(
+            self.method, replyroot)
+        ctx = plugins.message.unmarshalled(reply=result)
+        result = ctx.reply
+        if self.options.faults:
+            return result
+        return httplib.OK, result
+
+    def __get_fault(self, replyroot):
+        """
+        Extract fault information from a SOAP reply.
+
+        Returns an I{unmarshalled} fault L{Object} or None in case the given
+        XML document does not contain a SOAP <Fault> element.
 
         @param replyroot: A SOAP reply message root XML element or None.
-        @type replyroot: L{Element}
+        @type replyroot: L{Element}|I{None}
         @return: A fault object.
         @rtype: L{Object}
+
         """
         envns = suds.bindings.binding.envns
-        soapenv = replyroot and replyroot.getChild('Envelope', envns)
-        soapbody = soapenv and soapenv.getChild('Body', envns)
-        fault = soapbody and soapbody.getChild('Fault', envns)
+        soapenv = replyroot and replyroot.getChild("Envelope", envns)
+        soapbody = soapenv and soapenv.getChild("Body", envns)
+        fault = soapbody and soapbody.getChild("Fault", envns)
         return fault is not None and UmxBasic().process(fault)
 
-    def headers(self):
+    def __headers(self):
         """
-        Get HTTP headers or the HTTP/HTTPS request.
+        Get HTTP headers for a HTTP/HTTPS SOAP request.
+
         @return: A dictionary of header/values.
         @rtype: dict
+
         """
         action = self.method.soap.action
         if isinstance(action, unicode):
-            action = action.encode('utf-8')
-        stock = {'Content-Type':'text/xml; charset=utf-8', 'SOAPAction':action}
-        result = dict(stock, **self.options.headers)
-        log.debug('headers = %s', result)
+            action = action.encode("utf-8")
+        result = {
+            "Content-Type": "text/xml; charset=utf-8",
+            "SOAPAction": action}
+        result.update(**self.options.headers)
+        log.debug("headers = %s", result)
         return result
 
-    def location(self):
-        """
-        Returns the SOAP request's target location URL.
+    def __location(self):
+        """Returns the SOAP request's target location URL."""
+        return Unskin(self.options).get("location", self.method.location)
 
-        """
-        return Unskin(self.options).get('location', self.method.location)
 
-
-class SimClient(SoapClient):
+class _SimClient(_SoapClient):
     """
-    Loopback client used for message/reply simulation.
+    Loopback _SoapClient used for SOAP request/reply simulation.
+
+    Used when a web service operation is invoked with injected SOAP request or
+    reply data.
+
     """
 
-    injkey = '__inject'
+    __injkey = "__inject"
 
     @classmethod
     def simulation(cls, kwargs):
-        """ get whether loopback has been specified in the I{kwargs}. """
-        return kwargs.has_key(SimClient.injkey)
+        """Get whether injected data has been specified in I{kwargs}."""
+        return kwargs.has_key(_SimClient.__injkey)
 
     def invoke(self, args, kwargs):
         """
-        Send the required soap message to invoke the specified method
-        @param args: A list of args for the method invoked.
-        @type args: list
-        @param kwargs: Named (keyword) args for the method invoked.
+        Invoke a specified web service method.
+
+        Uses an injected SOAP request/response instead of a regularly
+        constructed/received one.
+
+        Depending on how the ``nosend`` & ``retxml`` options are set, may do
+        one of the following:
+          * Return a constructed web service operation request without sending
+            it to the web service.
+          * Invoke the web service operation and return its SOAP reply XML.
+          * Invoke the web service operation, process its results and return
+            the Python object representing the returned value.
+
+        @param args: Positional arguments for the method invoked.
+        @type args: list|tuple
+        @param kwargs: Keyword arguments for the method invoked.
         @type kwargs: dict
-        @return: The result of the method invocation.
-        @rtype: I{builtin} or I{subclass of} L{Object}
+        @return: SOAP request, SOAP reply or a web service return value.
+        @rtype: L{RequestContext}|I{builtin}|I{subclass of} L{Object}|I{bytes}|
+            I{None}
+
         """
-        simulation = kwargs[self.injkey]
-        msg = simulation.get('msg')
+        simulation = kwargs.pop(self.__injkey)
+        msg = simulation.get("msg")
         if msg is not None:
             assert msg.__class__ is suds.byte_str_class
             return self.send(_parse(msg))
         msg = self.method.binding.input.get_message(self.method, args, kwargs)
-        log.debug('inject (simulated) send message:\n%s', msg)
-        reply = simulation.get('reply')
+        log.debug("inject (simulated) send message:\n%s", msg)
+        reply = simulation.get("reply")
         if reply is not None:
             assert reply.__class__ is suds.byte_str_class
-            status = simulation.get('status')
-            description=simulation.get('description')
+            status = simulation.get("status")
+            description = simulation.get("description")
             if description is None:
-                description = 'injected reply'
-            return self.process_reply(reply=reply, status=status,
-                description=description, original_soapenv=msg)
-        raise Exception('reply or msg injection parameter expected');
+                description = "injected reply"
+            return self.process_reply(reply, status, description)
+        raise Exception("reply or msg injection parameter expected")
 
 
-class RequestContext:
-    """
-    A request context.
-    Returned when the ''nosend'' options is specified. Allows the caller to
-    take care of sending the request himself and simply return the reply data
-    for further processing.
-    @ivar client: The suds client.
-    @type client: L{Client}
-    @ivar envelope: The request SOAP envelope.
-    @type envelope: str
-    @ivar original_envelope: The original request SOAP envelope before plugin
-                             processing.
-    @type original_envelope: str
+def _parse(string):
     """
+    Parses given XML document content.
 
-    def __init__(self, client, envelope, original_envelope):
-        """
-        @param client: The suds client.
-        @type client: L{Client}
-        @param envelope: The request SOAP envelope.
-        @type envelope: str
-        @param original_envelope: The original request SOAP envelope before
-                                  plugin processing.
-        @type original_envelope: str
-        """
-        self.client = client
-        self.envelope = envelope
-        self.original_envelope = original_envelope
+    Returns the resulting root XML element node or None if the given XML
+    content is empty.
 
-    def process_reply(self, reply, status=None, description=None):
-        """
-        Re-entry for processing a successful reply.
-        @param reply: The reply SOAP envelope.
-        @type reply: str
-        @param status: The HTTP status code
-        @type status: int
-        @param description: Additional status description.
-        @type description: str
-        @return: The returned value for the invoked method.
-        @return: The result of the method invocation.
-        @rtype: I{builtin}|I{subclass of} L{Object}
-        """
-        return self.client.process_reply(reply=reply, status=status,
-            description=description, original_soapenv=self.original_envelope)
-
-
-def _parse(string):
-    """
-    Parses the given XML document content and returns the resulting root XML
-    element node. Returns None if the given XML content is empty.
     @param string: XML document content to parse.
-    @type string: str
+    @type string: I{bytes}
     @return: Resulting root XML element node or None.
-    @rtype: L{Element}
+    @rtype: L{Element}|I{None}
+
     """
-    if len(string) > 0:
-        return Parser().parse(string=string)
+    if string:
+        return suds.sax.parser.Parser().parse(string=string)
diff --git a/suds/mx/__init__.py b/suds/mx/__init__.py
index 719e52d..dec87a6 100644
--- a/suds/mx/__init__.py
+++ b/suds/mx/__init__.py
@@ -1,22 +1,21 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides modules containing classes to support
-marshalling (XML).
+Provides modules containing classes to support marshalling to XML.
+
 """
 
 from suds.sudsobject import Object
@@ -24,11 +23,13 @@ from suds.sudsobject import Object
 
 class Content(Object):
     """
-    Marshaller Content.
+    Marshaller content.
+
     @ivar tag: The content tag.
     @type tag: str
     @ivar value: The content's value.
     @type value: I{any}
+
     """
 
     extensions = []
@@ -39,21 +40,21 @@ class Content(Object):
         @type tag: str
         @param value: The content's value.
         @type value: I{any}
+
         """
         Object.__init__(self)
         self.tag = tag
         self.value = value
-        for k,v in kwargs.items():
+        for k, v in kwargs.iteritems():
             setattr(self, k, v)
 
     def __getattr__(self, name):
-        if name not in self.__dict__:
-            if name in self.extensions:
-                v = None
-                setattr(self, name, v)
-            else:
-                raise AttributeError, \
-                    'Content has no attribute %s' % name
-        else:
-            v = self.__dict__[name]
-        return v
+        try:
+            return self.__dict__[name]
+        except KeyError:
+            pass
+        if name in self.extensions:
+            value = None
+            setattr(self, name, value)
+            return value
+        raise AttributeError, "Content has no attribute %s" % (name,)
diff --git a/suds/mx/appender.py b/suds/mx/appender.py
index f900338..726e262 100644
--- a/suds/mx/appender.py
+++ b/suds/mx/appender.py
@@ -60,7 +60,7 @@ class ContentAppender:
         @param marshaller: A marshaller.
         @type marshaller: L{suds.mx.core.Core}
         """
-        self.default = PrimativeAppender(marshaller)
+        self.default = PrimitiveAppender(marshaller)
         self.appenders = (
             (Matcher(None), NoneAppender(marshaller)),
             (Matcher(null), NoneAppender(marshaller)),
@@ -168,9 +168,9 @@ class Appender:
         self.marshaller.append(parent, content)
 
 
-class PrimativeAppender(Appender):
+class PrimitiveAppender(Appender):
     """
-    An appender for python I{primative} types.
+    An appender for python I{primitive} types.
     """
 
     def append(self, parent, content):
diff --git a/suds/mx/literal.py b/suds/mx/literal.py
index 4b3480f..29cd6e6 100644
--- a/suds/mx/literal.py
+++ b/suds/mx/literal.py
@@ -223,7 +223,7 @@ class Typed(Core):
         """
         Translate using the XSD type information.
         Python I{dict} is translated to a suds object. Most importantly,
-        primative values are translated from python types to XML types using
+        primitive values are translated from python types to XML types using
         the XSD type.
         @param content: The content to translate.
         @type content: L{Object}
diff --git a/suds/mx/typer.py b/suds/mx/typer.py
index dc16fa5..d3a6829 100644
--- a/suds/mx/typer.py
+++ b/suds/mx/typer.py
@@ -115,5 +115,5 @@ class Typer:
             md = object.__metadata__
             known = md.sxtype
             return known
-        except:
+        except Exception:
             pass
diff --git a/suds/plugin.py b/suds/plugin.py
index 3579f5c..b957897 100644
--- a/suds/plugin.py
+++ b/suds/plugin.py
@@ -1,22 +1,21 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The plugin module provides classes for implementation
-of suds plugins.
+The plugin module provides suds plugin implementation classes.
+
 """
 
 from suds import *
@@ -26,17 +25,17 @@ log = getLogger(__name__)
 
 
 class Context(object):
-    """
-    Plugin context.
-    """
+    """Plugin context."""
     pass
 
 
 class InitContext(Context):
     """
     Init Context.
-    @ivar wsdl: The wsdl.
+
+    @ivar wsdl: The WSDL.
     @type wsdl: L{wsdl.Definitions}
+
     """
     pass
 
@@ -44,10 +43,12 @@ class InitContext(Context):
 class DocumentContext(Context):
     """
     The XML document load context.
+
     @ivar url: The URL.
     @type url: str
     @ivar document: Either the XML text or the B{parsed} document root.
     @type document: (str|L{sax.element.Element})
+
     """
     pass
 
@@ -55,120 +56,138 @@ class DocumentContext(Context):
 class MessageContext(Context):
     """
     The context for sending the SOAP envelope.
+
     @ivar envelope: The SOAP envelope to be sent.
     @type envelope: (str|L{sax.element.Element})
     @ivar reply: The reply.
     @type reply: (str|L{sax.element.Element}|object)
+
     """
     pass
 
 
 class Plugin:
-    """
-    Plugin base.
-    """
+    """Plugin base."""
     pass
 
 
 class InitPlugin(Plugin):
-    """
-    The base class for suds I{init} plugins.
-    """
+    """Base class for all suds I{init} plugins."""
 
     def initialized(self, context):
         """
         Suds client initialization.
-        Called after wsdl the has been loaded.  Provides the plugin
-        with the opportunity to inspect/modify the WSDL.
+
+        Called after WSDL the has been loaded. Provides the plugin with the
+        opportunity to inspect/modify the WSDL.
+
         @param context: The init context.
         @type context: L{InitContext}
+
         """
         pass
 
 
 class DocumentPlugin(Plugin):
-    """
-    The base class for suds I{document} plugins.
-    """
+    """Base class for suds I{document} plugins."""
 
     def loaded(self, context):
         """
-        Suds has loaded a WSDL/XSD document.  Provides the plugin
-        with an opportunity to inspect/modify the unparsed document.
-        Called after each WSDL/XSD document is loaded.
+        Suds has loaded a WSDL/XSD document.
+
+        Provides the plugin with an opportunity to inspect/modify the unparsed
+        document. Called after each WSDL/XSD document is loaded.
+
         @param context: The document context.
         @type context: L{DocumentContext}
+
         """
         pass
 
     def parsed(self, context):
         """
-        Suds has parsed a WSDL/XSD document.  Provides the plugin
-        with an opportunity to inspect/modify the parsed document.
-        Called after each WSDL/XSD document is parsed.
+        Suds has parsed a WSDL/XSD document.
+
+        Provides the plugin with an opportunity to inspect/modify the parsed
+        document. Called after each WSDL/XSD document is parsed.
+
         @param context: The document context.
         @type context: L{DocumentContext}
+
         """
         pass
 
 
 class MessagePlugin(Plugin):
-    """
-    The base class for suds I{SOAP message} plugins.
-    """
+    """Base class for suds I{SOAP message} plugins."""
 
     def marshalled(self, context):
         """
-        Suds will send the specified soap envelope.
-        Provides the plugin with the opportunity to inspect/modify
-        the envelope Document before it is sent.
+        Suds is about to send the specified SOAP envelope.
+
+        Provides the plugin with the opportunity to inspect/modify the envelope
+        Document before it is sent.
+
         @param context: The send context.
             The I{envelope} is the envelope document.
         @type context: L{MessageContext}
+
         """
         pass
 
     def sending(self, context):
         """
-        Suds will send the specified SOAP envelope.
-        Provides the plugin with the opportunity to inspect/modify
-        the message text it is sent.
+        Suds is about to send the specified SOAP envelope.
+
+        Provides the plugin with the opportunity to inspect/modify the message
+        text before it is sent.
+
         @param context: The send context.
             The I{envelope} is the envelope text.
         @type context: L{MessageContext}
+
         """
         pass
 
     def received(self, context):
         """
         Suds has received the specified reply.
-        Provides the plugin with the opportunity to inspect/modify
-        the received XML text before it is SAX parsed.
+
+        Provides the plugin with the opportunity to inspect/modify the received
+        XML text before it is SAX parsed.
+
         @param context: The reply context.
             The I{reply} is the raw text.
         @type context: L{MessageContext}
+
         """
         pass
 
     def parsed(self, context):
         """
-        Suds has sax parsed the received reply.
-        Provides the plugin with the opportunity to inspect/modify
-        the sax parsed DOM tree for the reply before it is unmarshalled.
+        Suds has SAX parsed the received reply.
+
+        Provides the plugin with the opportunity to inspect/modify the SAX
+        parsed DOM tree for the reply before it is unmarshalled.
+
         @param context: The reply context.
             The I{reply} is DOM tree.
         @type context: L{MessageContext}
+
         """
         pass
 
     def unmarshalled(self, context):
         """
         Suds has unmarshalled the received reply.
-        Provides the plugin with the opportunity to inspect/modify
-        the unmarshalled reply object before it is returned.
+
+        Provides the plugin with the opportunity to inspect/modify the
+        unmarshalled reply object before it is returned.
+
         @param context: The reply context.
             The I{reply} is unmarshalled suds object.
         @type context: L{MessageContext}
+
         """
         pass
 
@@ -176,45 +195,45 @@ class MessagePlugin(Plugin):
 class PluginContainer:
     """
     Plugin container provides easy method invocation.
+
     @ivar plugins: A list of plugin objects.
     @type plugins: [L{Plugin},]
     @cvar ctxclass: A dict of plugin method / context classes.
     @type ctxclass: dict
+
     """
 
-    domains = {\
+    domains = {
         'init': (InitContext, InitPlugin),
         'document': (DocumentContext, DocumentPlugin),
-        'message': (MessageContext, MessagePlugin ),
-    }
+        'message': (MessageContext, MessagePlugin)}
 
     def __init__(self, plugins):
         """
         @param plugins: A list of plugin objects.
         @type plugins: [L{Plugin},]
+
         """
         self.plugins = plugins
 
     def __getattr__(self, name):
         domain = self.domains.get(name)
-        if domain:
-            plugins = []
-            ctx, pclass = domain
-            for p in self.plugins:
-                if isinstance(p, pclass):
-                    plugins.append(p)
-            return PluginDomain(ctx, plugins)
-        else:
-            raise Exception, 'plugin domain (%s), invalid' % name
+        if not domain:
+            raise Exception, 'plugin domain (%s), invalid' % (name,)
+        ctx, pclass = domain
+        plugins = [p for p in self.plugins if isinstance(p, pclass)]
+        return PluginDomain(ctx, plugins)
 
 
 class PluginDomain:
     """
     The plugin domain.
+
     @ivar ctx: A context.
     @type ctx: L{Context}
     @ivar plugins: A list of plugins (targets).
     @type plugins: list
+
     """
 
     def __init__(self, ctx, plugins):
@@ -228,10 +247,12 @@ class PluginDomain:
 class Method:
     """
     Plugin method.
+
     @ivar name: The method name.
     @type name: str
     @ivar domain: The plugin domain.
     @type domain: L{PluginDomain}
+
     """
 
     def __init__(self, name, domain):
@@ -240,6 +261,7 @@ class Method:
         @type name: str
         @param domain: A plugin domain.
         @type domain: L{PluginDomain}
+
         """
         self.name = name
         self.domain = domain
@@ -248,10 +270,7 @@ class Method:
         ctx = self.domain.ctx()
         ctx.__dict__.update(kwargs)
         for plugin in self.domain.plugins:
-            try:
-                method = getattr(plugin, self.name, None)
-                if method and callable(method):
-                    method(ctx)
-            except Exception, pe:
-                log.exception(pe)
+            method = getattr(plugin, self.name, None)
+            if method and callable(method):
+                method(ctx)
         return ctx
diff --git a/suds/reader.py b/suds/reader.py
index ad71642..1454c34 100644
--- a/suds/reader.py
+++ b/suds/reader.py
@@ -1,166 +1,196 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-  XML document reader classes providing integration with the suds library's
+XML document reader classes providing integration with the suds library's
 caching system.
+
 """
 
+import suds.cache
+import suds.plugin
+import suds.sax.parser
+import suds.transport
 
-from suds.cache import Cache, NoCache
-from suds.plugin import PluginContainer
-from suds.sax.parser import Parser
-from suds.store import DocumentStore
-from suds.transport import Request
+try:
+    from hashlib import md5
+except ImportError:
+    # 'hashlib' package added in Python 2.5 so use the now deprecated/removed
+    # 'md5' package in older Python versions.
+    from md5 import md5
 
 
-class Reader:
+class Reader(object):
     """
     Provides integration with the cache.
+
     @ivar options: An options object.
     @type options: I{Options}
+
     """
 
     def __init__(self, options):
         """
         @param options: An options object.
         @type options: I{Options}
+
         """
         self.options = options
-        self.plugins = PluginContainer(options.plugins)
+        self.plugins = suds.plugin.PluginContainer(options.plugins)
 
     def mangle(self, name, x):
         """
         Mangle the name by hashing the I{name} and appending I{x}.
-        @return: the mangled name.
-        """
-        h = abs(hash(name))
-        return '%s-%s' % (h, x)
 
+        @return: The mangled name.
+        @rtype: str
 
-class DocumentReader(Reader):
-    """
-    Provides integration between the SAX L{Parser} and the document cache.
-    """
-
-    def open(self, url):
         """
-        Open an XML document at the specified I{URL}.
-        First, the document attempted to be retrieved from the I{object cache}.
-        If not found, it is downloaded and parsed using the SAX parser. The
-        result is added to the cache for the next open().
-        @param url: A document URL.
-        @type url: str.
-        @return: The specified XML document.
-        @rtype: I{Document}
-        """
-        cache = self.cache()
-        id = self.mangle(url, 'document')
-        d = cache.get(id)
-        if d is None:
-            d = self.download(url)
-            cache.put(id, d)
-        self.plugins.document.parsed(url=url, document=d.root())
-        return d
-
-    def download(self, url):
-        """
-        Download the document.
-        @param url: A document URL.
-        @type url: str.
-        @return: A file pointer to the document.
-        @rtype: file-like
-        """
-        content = None
-        store = self.options.documentStore
-        if store is not None:
-            content = store.open(url)
-        if content is None:
-            fp = self.options.transport.open(Request(url))
-            try:
-                content = fp.read()
-            finally:
-                fp.close()
-        ctx = self.plugins.document.loaded(url=url, document=content)
-        content = ctx.document
-        sax = Parser()
-        return sax.parse(string=content)
-
-    def cache(self):
-        """
-        Get the cache.
-        @return: The I{cache} when I{cachingpolicy} = B{0}.
-        @rtype: L{Cache}
-        """
-        if self.options.cachingpolicy == 0:
-            return self.options.cache
-        return NoCache()
+        h = md5(name.encode()).hexdigest()
+        return '%s-%s' % (h, x)
 
 
 class DefinitionsReader(Reader):
     """
-    Provides integration between the WSDL Definitions object and the object
-    cache.
-    @ivar fn: A factory function (constructor) used to
-        create the object not found in the cache.
+    Integrates between the WSDL Definitions object and the object cache.
+
+    @ivar fn: A factory function used to create objects not found in the cache.
     @type fn: I{Constructor}
+
     """
 
     def __init__(self, options, fn):
         """
         @param options: An options object.
         @type options: I{Options}
-        @param fn: A factory function (constructor) used to create the object
-            not found in the cache.
+        @param fn: A factory function used to create objects not found in the
+            cache.
         @type fn: I{Constructor}
+
         """
-        Reader.__init__(self, options)
+        super(DefinitionsReader, self).__init__(options)
         self.fn = fn
 
     def open(self, url):
         """
-        Open a WSDL at the specified I{URL}.
-        First, the WSDL attempted to be retrieved from
-        the I{object cache}.  After unpickled from the cache, the
-        I{options} attribute is restored.
-        If not found, it is downloaded and instantiated using the
-        I{fn} constructor and added to the cache for the next open().
+        Open a WSDL schema at the specified I{URL}.
+
+        First, the WSDL schema is looked up in the I{object cache}. If not
+        found, a new one constructed using the I{fn} factory function and the
+        result is cached for the next open().
+
         @param url: A WSDL URL.
         @type url: str.
         @return: The WSDL object.
         @rtype: I{Definitions}
+
         """
-        cache = self.cache()
-        id = self.mangle(url, 'wsdl')
-        d = cache.get(id)
-        if d is None:
-            d = self.fn(url, self.options)
-            cache.put(id, d)
+        cache = self.__cache()
+        id = self.mangle(url, "wsdl")
+        wsdl = cache.get(id)
+        if wsdl is None:
+            wsdl = self.fn(url, self.options)
+            cache.put(id, wsdl)
         else:
-            d.options = self.options
-            for imp in d.imports:
+            # Cached WSDL Definitions objects may have been created with
+            # different options so we update them here with our current ones.
+            wsdl.options = self.options
+            for imp in wsdl.imports:
                 imp.imported.options = self.options
-        return d
+        return wsdl
 
-    def cache(self):
+    def __cache(self):
         """
-        Get the cache.
+        Get the I{object cache}.
+
         @return: The I{cache} when I{cachingpolicy} = B{1}.
         @rtype: L{Cache}
+
         """
         if self.options.cachingpolicy == 1:
             return self.options.cache
-        return NoCache()
+        return suds.cache.NoCache()
+
+
+class DocumentReader(Reader):
+    """Integrates between the SAX L{Parser} and the document cache."""
+
+    def open(self, url):
+        """
+        Open an XML document at the specified I{URL}.
+
+        First, a preparsed document is looked up in the I{object cache}. If not
+        found, its content is fetched from an external source and parsed using
+        the SAX parser. The result is cached for the next open().
+
+        @param url: A document URL.
+        @type url: str.
+        @return: The specified XML document.
+        @rtype: I{Document}
+
+        """
+        cache = self.__cache()
+        id = self.mangle(url, "document")
+        xml = cache.get(id)
+        if xml is None:
+            xml = self.__fetch(url)
+            cache.put(id, xml)
+        self.plugins.document.parsed(url=url, document=xml.root())
+        return xml
+
+    def __cache(self):
+        """
+        Get the I{object cache}.
+
+        @return: The I{cache} when I{cachingpolicy} = B{0}.
+        @rtype: L{Cache}
+
+        """
+        if self.options.cachingpolicy == 0:
+            return self.options.cache
+        return suds.cache.NoCache()
+
+    def __fetch(self, url):
+        """
+        Fetch document content from an external source.
+
+        The document content will first be looked up in the registered document
+        store, and if not found there, downloaded using the registered
+        transport system.
+
+        Before being returned, the fetched document content first gets
+        processed by all the registered 'loaded' plugins.
+
+        @param url: A document URL.
+        @type url: str.
+        @return: A file pointer to the fetched document content.
+        @rtype: file-like
+
+        """
+        content = None
+        store = self.options.documentStore
+        if store is not None:
+            content = store.open(url)
+        if content is None:
+            request = suds.transport.Request(url)
+            fp = self.options.transport.open(request)
+            try:
+                content = fp.read()
+            finally:
+                fp.close()
+        ctx = self.plugins.document.loaded(url=url, document=content)
+        content = ctx.document
+        sax = suds.sax.parser.Parser()
+        return sax.parse(string=content)
diff --git a/suds/resolver.py b/suds/resolver.py
index 2a30169..0d741c6 100644
--- a/suds/resolver.py
+++ b/suds/resolver.py
@@ -459,7 +459,7 @@ class GraphResolver(TreeResolver):
             md = object.__metadata__
             known = md.sxtype
             return known
-        except:
+        except Exception:
             pass
 
 
diff --git a/suds/sax/__init__.py b/suds/sax/__init__.py
index be13823..cde5a7a 100644
--- a/suds/sax/__init__.py
+++ b/suds/sax/__init__.py
@@ -73,7 +73,7 @@ class Namespace:
     def xsd(cls, ns):
         try:
             return cls.w3(ns) and ns[1].endswith('XMLSchema')
-        except:
+        except Exception:
             pass
         return False
 
@@ -81,7 +81,7 @@ class Namespace:
     def xsi(cls, ns):
         try:
             return cls.w3(ns) and ns[1].endswith('XMLSchema-instance')
-        except:
+        except Exception:
             pass
         return False
 
@@ -93,7 +93,7 @@ class Namespace:
     def w3(cls, ns):
         try:
             return ns[1].startswith('http://www.w3.org')
-        except:
+        except Exception:
             pass
         return False
 
@@ -101,6 +101,6 @@ class Namespace:
     def isns(cls, ns):
         try:
             return isinstance(ns, tuple) and len(ns) == len(cls.default)
-        except:
+        except Exception:
             pass
         return False
diff --git a/suds/sax/date.py b/suds/sax/date.py
index c5e5e93..6418796 100644
--- a/suds/sax/date.py
+++ b/suds/sax/date.py
@@ -1,4 +1,6 @@
-# This program is free software; you can redistribute it and/or modify
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
 # published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
@@ -357,7 +359,7 @@ def _bump_up_time_by_microsecond(time):
     cycling around silently to 00:00:00.0 in case of an overflow.
 
     @param time: Time object.
-    @type value: B{datetime}.I{time}
+    @type time: B{datetime}.I{time}
     @return: Time object.
     @rtype: B{datetime}.I{time}
 
@@ -376,7 +378,7 @@ def _date_from_match(match_object):
     _RE_DATETIME.
 
     @param match_object: The regular expression match.
-    @type value: B{re}.I{MatchObject}
+    @type match_object: B{re}.I{MatchObject}
     @return: A date object.
     @rtype: B{datetime}.I{date}
 
@@ -401,7 +403,7 @@ def _time_from_match(match_object):
     _RE_TIME.
 
     @param match_object: The regular expression match.
-    @type value: B{re}.I{MatchObject}
+    @type match_object: B{re}.I{MatchObject}
     @return: Time object + rounding flag.
     @rtype: tuple of B{datetime}.I{time} and bool
 
@@ -428,7 +430,7 @@ def _tzinfo_from_match(match_object):
     or _RE_TIME.
 
     @param match_object: The regular expression match.
-    @type value: B{re}.I{MatchObject}
+    @type match_object: B{re}.I{MatchObject}
     @return: A timezone information object.
     @rtype: B{datetime}.I{tzinfo}
 
diff --git a/suds/sax/document.py b/suds/sax/document.py
index 7a4a615..1a70a61 100644
--- a/suds/sax/document.py
+++ b/suds/sax/document.py
@@ -23,7 +23,7 @@ from suds.sax import *
 from suds.sax.element import Element
 
 
-class Document:
+class Document(UnicodeMixin):
     """ An XML Document """
 
     DECL = '<?xml version="1.0" encoding="UTF-8"?>'
diff --git a/suds/sax/element.py b/suds/sax/element.py
index 084ea2b..99f7010 100644
--- a/suds/sax/element.py
+++ b/suds/sax/element.py
@@ -1,21 +1,21 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides XML I{element} classes.
+XML I{element} classes.
+
 """
 
 from suds import *
@@ -27,47 +27,52 @@ from suds.sax.attribute import Attribute
 class Element(UnicodeMixin):
     """
     An XML element object.
-    @ivar parent: The node containing this attribute
+
+    @ivar parent: The node containing this attribute.
     @type parent: L{Element}
     @ivar prefix: The I{optional} namespace prefix.
     @type prefix: basestring
-    @ivar name: The I{unqualified} name of the attribute
+    @ivar name: The I{unqualified} name of the attribute.
     @type name: basestring
     @ivar expns: An explicit namespace (xmlns="...").
     @type expns: (I{prefix}, I{name})
     @ivar nsprefixes: A mapping of prefixes to namespaces.
     @type nsprefixes: dict
     @ivar attributes: A list of XML attributes.
-    @type attributes: [I{Attribute},]
+    @type attributes: [I{Attribute},...]
     @ivar text: The element's I{text} content.
     @type text: basestring
     @ivar children: A list of child elements.
-    @type children: [I{Element},]
+    @type children: [I{Element},...]
     @cvar matcher: A collection of I{lambda} for string matching.
     @cvar specialprefixes: A dictionary of builtin-special prefixes.
+
     """
 
     matcher = {
-        'eq': lambda a,b: a == b,
-        'startswith' : lambda a,b: a.startswith(b),
-        'endswith' : lambda a,b: a.endswith(b),
-        'contains' : lambda a,b: b in a}
+        "eq": lambda a, b: a == b,
+        "startswith": lambda a, b: a.startswith(b),
+        "endswith": lambda a, b: a.endswith(b),
+        "contains": lambda a, b: b in a}
 
-    specialprefixes = {Namespace.xmlns[0] : Namespace.xmlns[1]}
+    specialprefixes = {Namespace.xmlns[0]: Namespace.xmlns[1]}
 
     @classmethod
     def buildPath(self, parent, path):
-        """Build the specifed path as a/b/c.
+        """
+        Build the specified path as a/b/c.
 
         Any missing intermediate nodes are built automatically.
+
         @param parent: A parent element on which the path is built.
         @type parent: I{Element}
         @param path: A simple path separated by (/).
         @type path: basestring
         @return: The leaf node of I{path}.
         @rtype: L{Element}
+
         """
-        for tag in path.split('/'):
+        for tag in path.split("/"):
             child = parent.getChild(tag)
             if child is None:
                 child = Element(tag, parent)
@@ -82,42 +87,43 @@ class Element(UnicodeMixin):
         @type parent: I{Element}
         @param ns: An optional namespace.
         @type ns: (I{prefix}, I{name})
+
         """
         self.rename(name)
         self.expns = None
         self.nsprefixes = {}
         self.attributes = []
         self.text = None
-        if parent is not None:
-            if isinstance(parent, Element):
-                self.parent = parent
-            else:
-                raise Exception('parent (%s) not-valid', parent.__class__.__name__)
-        else:
-            self.parent = None
+        if parent is not None and not isinstance(parent, Element):
+            raise Exception("parent (%s) not-valid" %
+                (parent.__class__.__name__,))
+        self.parent = parent
         self.children = []
         self.applyns(ns)
 
     def rename(self, name):
         """
         Rename the element.
+
         @param name: A new name for the element.
         @type name: basestring
+
         """
         if name is None:
-            raise Exception('name (%s) not-valid' % name)
-        else:
-            self.prefix, self.name = splitPrefix(name)
+            raise Exception("name (%s) not-valid" % (name,))
+        self.prefix, self.name = splitPrefix(name)
 
     def setPrefix(self, p, u=None):
         """
         Set the element namespace prefix.
+
         @param p: A new prefix for the element.
         @type p: basestring
         @param u: A namespace URI to be mapped to the prefix.
         @type u: basestring
         @return: self
         @rtype: L{Element}
+
         """
         self.prefix = p
         if p is not None and u is not None:
@@ -127,19 +133,23 @@ class Element(UnicodeMixin):
 
     def qname(self):
         """
-        Get the B{fully} qualified name of this element
+        Get this element's B{fully} qualified name.
+
         @return: The fully qualified name.
         @rtype: basestring
+
         """
         if self.prefix is None:
             return self.name
-        return '%s:%s' % (self.prefix, self.name)
+        return "%s:%s" % (self.prefix, self.name)
 
     def getRoot(self):
         """
         Get the root (top) node of the tree.
+
         @return: The I{top} node of this tree.
         @rtype: I{Element}
+
         """
         if self.parent is None:
             return self
@@ -148,26 +158,30 @@ class Element(UnicodeMixin):
     def clone(self, parent=None):
         """
         Deep clone of this element and children.
+
         @param parent: An optional parent for the copied fragment.
         @type parent: I{Element}
         @return: A deep copy parented by I{parent}
         @rtype: I{Element}
+
         """
         root = Element(self.qname(), parent, self.namespace())
         for a in self.attributes:
             root.append(a.clone(self))
         for c in self.children:
             root.append(c.clone(self))
-        for item in self.nsprefixes.items():
-            root.addPrefix(item[0], item[1])
+        for ns in self.nsprefixes.items():
+            root.addPrefix(ns[0], ns[1])
         return root
 
     def detach(self):
         """
         Detach from parent.
-        @return: This element removed from its parent's
-            child list and I{parent}=I{None}
+
+        @return: This element removed from its parent's child list and
+            I{parent}=I{None}.
         @rtype: L{Element}
+
         """
         if self.parent is not None:
             if self in self.parent.children:
@@ -178,11 +192,13 @@ class Element(UnicodeMixin):
     def set(self, name, value):
         """
         Set an attribute's value.
+
         @param name: The name of the attribute.
         @type name: basestring
         @param value: The attribute value.
         @type value: basestring
         @see: __setitem__()
+
         """
         attr = self.getAttribute(name)
         if attr is None:
@@ -194,31 +210,35 @@ class Element(UnicodeMixin):
     def unset(self, name):
         """
         Unset (remove) an attribute.
+
         @param name: The attribute name.
         @type name: str
         @return: self
         @rtype: L{Element}
+
         """
         try:
             attr = self.getAttribute(name)
             self.attributes.remove(attr)
-        except:
+        except Exception:
             pass
         return self
 
     def get(self, name, ns=None, default=None):
         """
         Get the value of an attribute by name.
+
         @param name: The name of the attribute.
         @type name: basestring
         @param ns: The optional attribute's namespace.
         @type ns: (I{prefix}, I{name})
-        @param default: An optional value to be returned when either
-            the attribute does not exist of has not value.
+        @param default: An optional value to be returned when either the
+            attribute does not exist or has no value.
         @type default: basestring
-        @return: The attribute's value or I{default}
+        @return: The attribute's value or I{default}.
         @rtype: basestring
         @see: __getitem__()
+
         """
         attr = self.getAttribute(name, ns)
         if attr is None or attr.value is None:
@@ -228,24 +248,27 @@ class Element(UnicodeMixin):
     def setText(self, value):
         """
         Set the element's L{Text} content.
+
         @param value: The element's text value.
         @type value: basestring
         @return: self
         @rtype: I{Element}
+
         """
-        if isinstance(value, Text):
-            self.text = value
-        else:
-            self.text = Text(value)
+        if not isinstance(value, Text):
+            value = Text(value)
+        self.text = value
         return self
 
     def getText(self, default=None):
         """
-        Get the element's L{Text} content with optional default
+        Get the element's L{Text} content with optional default.
+
         @param default: A value to be returned when no text content exists.
         @type default: basestring
-        @return: The text content, or I{default}
+        @return: The text content, or I{default}.
         @rtype: L{Text}
+
         """
         if self.hasText():
             return self.text
@@ -254,8 +277,10 @@ class Element(UnicodeMixin):
     def trim(self):
         """
         Trim leading and trailing whitespace.
+
         @return: self
         @rtype: L{Element}
+
         """
         if self.hasText():
             self.text = self.text.trim()
@@ -263,19 +288,22 @@ class Element(UnicodeMixin):
 
     def hasText(self):
         """
-        Get whether the element has I{text} and that it is not an empty
-        (zero length) string.
+        Get whether the element has non-empty I{text} string.
+
         @return: True when has I{text}.
         @rtype: boolean
+
         """
-        return self.text is not None and len(self.text)
+        return bool(self.text)
 
     def namespace(self):
         """
         Get the element's namespace.
+
         @return: The element's namespace by resolving the prefix, the explicit
             namespace or the inherited namespace.
         @rtype: (I{prefix}, I{name})
+
         """
         if self.prefix is None:
             return self.defaultNamespace()
@@ -284,10 +312,13 @@ class Element(UnicodeMixin):
     def defaultNamespace(self):
         """
         Get the default (unqualified namespace).
-        This is the expns of the first node (looking up the tree)
-        that has it set.
+
+        This is the expns of the first node (looking up the tree) that has it
+        set.
+
         @return: The namespace of a node when not qualified.
         @rtype: (I{prefix}, I{name})
+
         """
         p = self
         while p is not None:
@@ -300,11 +331,13 @@ class Element(UnicodeMixin):
         """
         Append the specified child based on whether it is an element or an
         attribute.
+
         @param objects: A (single|collection) of attribute(s) or element(s)
             to be added as children.
         @type objects: (L{Element}|L{Attribute})
         @return: self
         @rtype: L{Element}
+
         """
         if not isinstance(objects, (list, tuple)):
             objects = (objects,)
@@ -317,12 +350,14 @@ class Element(UnicodeMixin):
                 self.attributes.append(child)
                 child.parent = self
                 continue
-            raise Exception('append %s not-valid' % child.__class__.__name__)
+            raise Exception("append %s not-valid" %
+                (child.__class__.__name__,))
         return self
 
     def insert(self, objects, index=0):
         """
         Insert an L{Element} content at the specified index.
+
         @param objects: A (single|collection) of attribute(s) or element(s) to
             be added as children.
         @type objects: (L{Element}|L{Attribute})
@@ -330,40 +365,44 @@ class Element(UnicodeMixin):
         @type index: int
         @return: self
         @rtype: L{Element}
+
         """
         objects = (objects,)
         for child in objects:
-            if isinstance(child, Element):
-                self.children.insert(index, child)
-                child.parent = self
-            else:
-                raise Exception('append %s not-valid' % child.__class__.__name__)
+            if not isinstance(child, Element):
+                raise Exception("append %s not-valid" %
+                    (child.__class__.__name__,))
+            self.children.insert(index, child)
+            child.parent = self
         return self
 
     def remove(self, child):
         """
         Remove the specified child element or attribute.
+
         @param child: A child to remove.
         @type child: L{Element}|L{Attribute}
         @return: The detached I{child} when I{child} is an element, else None.
         @rtype: L{Element}|None
+
         """
         if isinstance(child, Element):
             return child.detach()
         if isinstance(child, Attribute):
             self.attributes.remove(child)
-        return None
 
     def replaceChild(self, child, content):
         """
         Replace I{child} with the specified I{content}.
+
         @param child: A child element.
         @type child: L{Element}
         @param content: An element or collection of elements.
-        @type content: L{Element} or [L{Element},]
+        @type content: L{Element} or [L{Element},...]
+
         """
         if child not in self.children:
-            raise Exception('child not-found')
+            raise Exception("child not-found")
         index = self.children.index(child)
         self.remove(child)
         if not isinstance(content, (list, tuple)):
@@ -375,7 +414,8 @@ class Element(UnicodeMixin):
 
     def getAttribute(self, name, ns=None, default=None):
         """
-        Get an attribute by name and (optional) namespace
+        Get an attribute by name and (optional) namespace.
+
         @param name: The name of a contained attribute (may contain prefix).
         @type name: basestring
         @param ns: An optional namespace
@@ -384,12 +424,11 @@ class Element(UnicodeMixin):
         @type default: L{Attribute}
         @return: The requested attribute object.
         @rtype: L{Attribute}
+
         """
         if ns is None:
             prefix, name = splitPrefix(name)
-            if prefix is None:
-                ns = None
-            else:
+            if prefix is not None:
                 ns = self.resolvePrefix(prefix)
         for a in self.attributes:
             if a.match(name, ns):
@@ -399,6 +438,7 @@ class Element(UnicodeMixin):
     def getChild(self, name, ns=None, default=None):
         """
         Get a child by (optional) name and/or (optional) namespace.
+
         @param name: The name of a child element (may contain prefix).
         @type name: basestring
         @param ns: An optional namespace used to match the child.
@@ -407,12 +447,11 @@ class Element(UnicodeMixin):
         @type default: L{Element}
         @return: The requested child, or I{default} when not-found.
         @rtype: L{Element}
+
         """
         if ns is None:
             prefix, name = splitPrefix(name)
-            if prefix is None:
-                ns = None
-            else:
+            if prefix is not None:
                 ns = self.resolvePrefix(prefix)
         for c in self.children:
             if c.match(name, ns):
@@ -421,68 +460,73 @@ class Element(UnicodeMixin):
 
     def childAtPath(self, path):
         """
-        Get a child at I{path} where I{path} is a (/) separated
-        list of element names that are expected to be children.
+        Get a child at I{path} where I{path} is a (/) separated list of element
+        names that are expected to be children.
+
         @param path: A (/) separated list of element names.
         @type path: basestring
-        @return: The leaf node at the end of I{path}
+        @return: The leaf node at the end of I{path}.
         @rtype: L{Element}
+
         """
         result = None
         node = self
-        for name in [p for p in path.split('/') if len(p) > 0]:
+        for name in path.split("/"):
+            if not name:
+                continue
             ns = None
             prefix, name = splitPrefix(name)
             if prefix is not None:
                 ns = node.resolvePrefix(prefix)
             result = node.getChild(name, ns)
             if result is None:
-                break;
-            else:
-                node = result
+                return
+            node = result
         return result
 
     def childrenAtPath(self, path):
         """
-        Get a list of children at I{path} where I{path} is a (/) separated
-        list of element names that are expected to be children.
+        Get a list of children at I{path} where I{path} is a (/) separated list
+        of element names expected to be children.
+
         @param path: A (/) separated list of element names.
         @type path: basestring
-        @return: The collection leaf nodes at the end of I{path}
+        @return: The collection leaf nodes at the end of I{path}.
         @rtype: [L{Element},...]
+
         """
-        parts = [p for p in path.split('/') if len(p) > 0]
+        parts = [p for p in path.split("/") if p]
         if len(parts) == 1:
-            result = self.getChildren(path)
-        else:
-            result = self.__childrenAtPath(parts)
-        return result
+            return self.getChildren(path)
+        return self.__childrenAtPath(parts)
 
     def getChildren(self, name=None, ns=None):
         """
         Get a list of children by (optional) name and/or (optional) namespace.
-        @param name: The name of a child element (may contain prefix).
+
+        @param name: The name of a child element (may contain a prefix).
         @type name: basestring
         @param ns: An optional namespace used to match the child.
         @type ns: (I{prefix}, I{name})
         @return: The list of matching children.
         @rtype: [L{Element},...]
+
         """
         if ns is None:
             if name is None:
                 return self.children
             prefix, name = splitPrefix(name)
-            if prefix is None:
-                ns = None
-            else:
+            if prefix is not None:
                 ns = self.resolvePrefix(prefix)
         return [c for c in self.children if c.match(name, ns)]
 
     def detachChildren(self):
         """
         Detach and return this element's children.
+
         @return: The element's children (detached).
         @rtype: [L{Element},...]
+
         """
         detached = self.children
         self.children = []
@@ -492,17 +536,19 @@ class Element(UnicodeMixin):
 
     def resolvePrefix(self, prefix, default=Namespace.default):
         """
-        Resolve the specified prefix to a namespace.  The I{nsprefixes} is
-        searched.  If not found, it walks up the tree until either resolved or
-        the top of the tree is reached.  Searching up the tree provides for
+        Resolve the specified prefix to a namespace. The I{nsprefixes} is
+        searched. If not found, walk up the tree until either resolved or the
+        top of the tree is reached. Searching up the tree provides for
         inherited mappings.
+
         @param prefix: A namespace prefix to resolve.
         @type prefix: basestring
-        @param default: An optional value to be returned when the prefix
-            cannot be resolved.
-        @type default: (I{prefix},I{URI})
+        @param default: An optional value to be returned when the prefix cannot
+            be resolved.
+        @type default: (I{prefix}, I{URI})
         @return: The namespace that is mapped to I{prefix} in this context.
-        @rtype: (I{prefix},I{URI})
+        @rtype: (I{prefix}, I{URI})
+
         """
         n = self
         while n is not None:
@@ -516,6 +562,7 @@ class Element(UnicodeMixin):
     def addPrefix(self, p, u):
         """
         Add or update a prefix mapping.
+
         @param p: A prefix.
         @type p: basestring
         @param u: A namespace URI.
@@ -529,6 +576,7 @@ class Element(UnicodeMixin):
     def updatePrefix(self, p, u):
         """
         Update (redefine) a prefix mapping for the branch.
+
         @param p: A prefix.
         @type p: basestring
         @param u: A namespace URI.
@@ -536,6 +584,7 @@ class Element(UnicodeMixin):
         @return: self
         @rtype: L{Element}
         @note: This method traverses down the entire branch!
+
         """
         if p in self.nsprefixes:
             self.nsprefixes[p] = u
@@ -546,10 +595,12 @@ class Element(UnicodeMixin):
     def clearPrefix(self, prefix):
         """
         Clear the specified prefix from the prefix mappings.
+
         @param prefix: A prefix to clear.
         @type prefix: basestring
         @return: self
         @rtype: L{Element}
+
         """
         if prefix in self.nsprefixes:
             del self.nsprefixes[prefix]
@@ -558,38 +609,42 @@ class Element(UnicodeMixin):
     def findPrefix(self, uri, default=None):
         """
         Find the first prefix that has been mapped to a namespace URI.
-        The local mapping is searched, then it walks up the tree until
-        it reaches the top or finds a match.
+
+        The local mapping is searched, then walks up the tree until it reaches
+        the top or finds a match.
+
         @param uri: A namespace URI.
         @type uri: basestring
         @param default: A default prefix when not found.
         @type default: basestring
         @return: A mapped prefix.
         @rtype: basestring
+
         """
         for item in self.nsprefixes.items():
             if item[1] == uri:
-                prefix = item[0]
-                return prefix
+                return item[0]
         for item in self.specialprefixes.items():
             if item[1] == uri:
-                prefix = item[0]
-                return prefix
+                return item[0]
         if self.parent is not None:
             return self.parent.findPrefix(uri, default)
         return default
 
-    def findPrefixes(self, uri, match='eq'):
+    def findPrefixes(self, uri, match="eq"):
         """
         Find all prefixes that have been mapped to a namespace URI.
-        The local mapping is searched, then it walks up the tree until it
-        reaches the top, collecting all matches.
+
+        The local mapping is searched, then walks up the tree until it reaches
+        the top, collecting all matches.
+
         @param uri: A namespace URI.
         @type uri: basestring
         @param match: A matching function L{Element.matcher}.
         @type match: basestring
         @return: A list of mapped prefixes.
         @rtype: [basestring,...]
+
         """
         result = []
         for item in self.nsprefixes.items():
@@ -606,18 +661,21 @@ class Element(UnicodeMixin):
 
     def promotePrefixes(self):
         """
-        Push prefix declarations up the tree as far as possible.  Prefix
-        mapping are pushed to its parent unless the parent has the
-        prefix mapped to another URI or the parent has the prefix.
-        This is propagated up the tree until the top is reached.
+        Push prefix declarations up the tree as far as possible.
+
+        Prefix mapping are pushed to its parent unless the parent has the
+        prefix mapped to another URI or the parent has the prefix. This is
+        propagated up the tree until the top is reached.
+
         @return: self
         @rtype: L{Element}
+
         """
         for c in self.children:
             c.promotePrefixes()
         if self.parent is None:
             return
-        for p,u in self.nsprefixes.items():
+        for p, u in self.nsprefixes.items():
             if p in self.parent.nsprefixes:
                 pu = self.parent.nsprefixes[p]
                 if pu == u:
@@ -630,10 +688,12 @@ class Element(UnicodeMixin):
 
     def refitPrefixes(self):
         """
-        Refit namespace qualification by replacing prefixes
-        with explicit namespaces. Also purges prefix mapping table.
+        Refit namespace qualification by replacing prefixes with explicit
+        namespaces. Also purges prefix mapping table.
+
         @return: self
         @rtype: L{Element}
+
         """
         for c in self.children:
             c.refitPrefixes()
@@ -648,11 +708,14 @@ class Element(UnicodeMixin):
     def normalizePrefixes(self):
         """
         Normalize the namespace prefixes.
-        This generates unique prefixes for all namespaces.  Then retrofits all
-        prefixes and prefix mappings.  Further, it will retrofix attribute values
-        that have values containing (:).
+
+        This generates unique prefixes for all namespaces. Then retrofits all
+        prefixes and prefix mappings. Further, it will retrofix attribute
+        values that have values containing (:).
+
         @return: self
         @rtype: L{Element}
+
         """
         PrefixNormalizer.apply(self)
         return self
@@ -660,40 +723,46 @@ class Element(UnicodeMixin):
     def isempty(self, content=True):
         """
         Get whether the element has no children.
+
         @param content: Test content (children & text) only.
         @type content: boolean
         @return: True when element has not children.
         @rtype: boolean
+
         """
-        noattrs = not len(self.attributes)
-        nochildren = not len(self.children)
-        notext = ( self.text is None )
-        nocontent = ( nochildren and notext )
+        nochildren = not self.children
+        notext = self.text is None
+        nocontent = nochildren and notext
         if content:
             return nocontent
+        noattrs = not len(self.attributes)
         return nocontent and noattrs
 
     def isnil(self):
         """
-        Get whether the element is I{nil} as defined by having
-        an attribute in the I{xsi:nil="true"}
+        Get whether the element is I{nil} as defined by having an
+        I{xsi:nil="true"} attribute.
+
         @return: True if I{nil}, else False
         @rtype: boolean
+
         """
-        nilattr = self.getAttribute('nil', ns=Namespace.xsins)
-        return nilattr is not None and ( nilattr.getValue().lower() == 'true' )
+        nilattr = self.getAttribute("nil", ns=Namespace.xsins)
+        return nilattr is not None and (nilattr.getValue().lower() == "true")
 
     def setnil(self, flag=True):
         """
-        Set this node to I{nil} as defined by having an
-        attribute I{xsi:nil}=I{flag}.
+        Set this node to I{nil} as defined by having an I{xsi:nil}=I{flag}
+        attribute.
+
         @param flag: A flag indicating how I{xsi:nil} will be set.
         @type flag: boolean
         @return: self
         @rtype: L{Element}
+
         """
         p, u = Namespace.xsins
-        name  = ':'.join((p, 'nil'))
+        name = ":".join((p, "nil"))
         self.set(name, str(flag).lower())
         self.addPrefix(p, u)
         if flag:
@@ -702,16 +771,20 @@ class Element(UnicodeMixin):
 
     def applyns(self, ns):
         """
-        Apply the namespace to this node.  If the prefix is I{None} then
-        this element's explicit namespace I{expns} is set to the
-        URI defined by I{ns}.  Otherwise, the I{ns} is simply mapped.
+        Apply the namespace to this node.
+
+        If the prefix is I{None} then this element's explicit namespace
+        I{expns} is set to the URI defined by I{ns}. Otherwise, the I{ns} is
+        simply mapped.
+
         @param ns: A namespace.
-        @type ns: (I{prefix},I{URI})
+        @type ns: (I{prefix}, I{URI})
+
         """
         if ns is None:
             return
         if not isinstance(ns, (tuple, list)):
-            raise Exception('namespace must be tuple')
+            raise Exception("namespace must be tuple")
         if ns[0] is None:
             self.expns = ns[1]
         else:
@@ -721,59 +794,63 @@ class Element(UnicodeMixin):
     def str(self, indent=0):
         """
         Get a string representation of this XML fragment.
+
         @param indent: The indent to be used in formatting the output.
         @type indent: int
         @return: A I{pretty} string.
         @rtype: basestring
+
         """
-        tab = '%*s'%(indent*3,'')
+        tab = "%*s" % (indent * 3, "")
         result = []
-        result.append('%s<%s' % (tab, self.qname()))
+        result.append("%s<%s" % (tab, self.qname()))
         result.append(self.nsdeclarations())
-        for a in [unicode(a) for a in self.attributes]:
-            result.append(' %s' % a)
+        for a in self.attributes:
+            result.append(" %s" % (unicode(a),))
         if self.isempty():
-            result.append('/>')
-            return ''.join(result)
-        result.append('>')
+            result.append("/>")
+            return "".join(result)
+        result.append(">")
         if self.hasText():
             result.append(self.text.escape())
         for c in self.children:
-            result.append('\n')
-            result.append(c.str(indent+1))
+            result.append("\n")
+            result.append(c.str(indent + 1))
         if len(self.children):
-            result.append('\n%s' % tab)
-        result.append('</%s>' % self.qname())
-        return ''.join(result)
+            result.append("\n%s" % (tab,))
+        result.append("</%s>" % (self.qname(),))
+        return "".join(result)
 
     def plain(self):
         """
         Get a string representation of this XML fragment.
+
         @return: A I{plain} string.
         @rtype: basestring
+
         """
-        result = []
-        result.append('<%s' % self.qname())
-        result.append(self.nsdeclarations())
-        for a in [unicode(a) for a in self.attributes]:
-            result.append(' %s' % a)
+        result = ["<%s" % (self.qname(),), self.nsdeclarations()]
+        for a in self.attributes:
+            result.append(" %s" % (unicode(a),))
         if self.isempty():
-            result.append('/>')
-            return ''.join(result)
-        result.append('>')
+            result.append("/>")
+            return "".join(result)
+        result.append(">")
         if self.hasText():
             result.append(self.text.escape())
         for c in self.children:
             result.append(c.plain())
-        result.append('</%s>' % self.qname())
-        return ''.join(result)
+        result.append("</%s>" % (self.qname(),))
+        return "".join(result)
 
     def nsdeclarations(self):
         """
-        Get a string representation for all namespace declarations
-        as xmlns="" and xmlns:p="".
+        Get a string representation for all namespace declarations as xmlns=""
+        and xmlns:p="".
+
         @return: A separated list of declarations.
         @rtype: basestring
+
         """
         s = []
         myns = (None, self.expns)
@@ -783,36 +860,39 @@ class Element(UnicodeMixin):
             pns = (None, self.parent.expns)
         if myns[1] != pns[1]:
             if self.expns is not None:
-                d = ' xmlns="%s"' % self.expns
-                s.append(d)
+                s.append(' xmlns="%s"' % (self.expns,))
         for item in self.nsprefixes.items():
-            (p,u) = item
+            p, u = item
             if self.parent is not None:
                 ns = self.parent.resolvePrefix(p)
-                if ns[1] == u: continue
-            d = ' xmlns:%s="%s"' % (p, u)
-            s.append(d)
-        return ''.join(s)
+                if ns[1] == u:
+                    continue
+            s.append(' xmlns:%s="%s"' % (p, u))
+        return "".join(s)
 
     def match(self, name=None, ns=None):
         """
         Match by (optional) name and/or (optional) namespace.
+
         @param name: The optional element tag name.
         @type name: str
         @param ns: An optional namespace.
         @type ns: (I{prefix}, I{name})
         @return: True if matched.
         @rtype: boolean
+
         """
-        byname = name is None or ( self.name == name )
-        byns = ns is None or ( self.namespace()[1] == ns[1] )
+        byname = name is None or (self.name == name)
+        byns = ns is None or (self.namespace()[1] == ns[1])
         return byname and byns
 
     def branch(self):
         """
         Get a flattened representation of the branch.
+
         @return: A flat list of nodes.
-        @rtype: [L{Element},..]
+        @rtype: [L{Element},...]
+
         """
         branch = [self]
         for c in self.children:
@@ -822,8 +902,10 @@ class Element(UnicodeMixin):
     def ancestors(self):
         """
         Get a list of ancestors.
+
         @return: A list of ancestors.
-        @rtype: [L{Element},..]
+        @rtype: [L{Element},...]
+
         """
         ancestors = []
         p = self.parent
@@ -835,9 +917,12 @@ class Element(UnicodeMixin):
     def walk(self, visitor):
         """
         Walk the branch and call the visitor function on each node.
+
         @param visitor: A function.
+        @type visitor: single argument function
         @return: self
         @rtype: L{Element}
+
         """
         visitor(self)
         for c in self.children:
@@ -845,9 +930,7 @@ class Element(UnicodeMixin):
         return self
 
     def prune(self):
-        """
-        Prune the branch of empty nodes.
-        """
+        """Prune the branch of empty nodes."""
         pruned = []
         for c in self.children:
             c.prune()
@@ -859,9 +942,8 @@ class Element(UnicodeMixin):
     def __childrenAtPath(self, parts):
         result = []
         node = self
-        last = len(parts)-1
-        ancestors = parts[:last]
-        leaf = parts[last]
+        ancestors = parts[:-1]
+        leaf = parts[-1]
         for name in ancestors:
             ns = None
             prefix, name = splitPrefix(name)
@@ -870,8 +952,7 @@ class Element(UnicodeMixin):
             child = node.getChild(name, ns)
             if child is None:
                 break
-            else:
-                node = child
+            node = child
         if child is not None:
             ns = None
             prefix, leaf = splitPrefix(leaf)
@@ -897,11 +978,11 @@ class Element(UnicodeMixin):
                 self.children.insert(index, value)
 
     def __eq__(self, rhs):
-        return isinstance(rhs, Element) and  \
-            self.match(rhs.name, rhs.namespace())
+        return (isinstance(rhs, Element) and
+            self.match(rhs.name, rhs.namespace()))
 
     def __repr__(self):
-        return 'Element (prefix=%s, name=%s)' % (self.prefix, self.name)
+        return "Element (prefix=%s, name=%s)" % (self.prefix, self.name)
 
     def __unicode__(self):
         return self.str()
@@ -913,16 +994,19 @@ class Element(UnicodeMixin):
 class NodeIterator:
     """
     The L{Element} child node iterator.
+
     @ivar pos: The current position
     @type pos: int
     @ivar children: A list of a child nodes.
-    @type children: [L{Element},..]
+    @type children: [L{Element},...]
+
     """
 
     def __init__(self, parent):
         """
         @param parent: An element to iterate.
         @type parent: L{Element}
+
         """
         self.pos = 0
         self.children = parent.children
@@ -930,47 +1014,53 @@ class NodeIterator:
     def next(self):
         """
         Get the next child.
+
         @return: The next child.
         @rtype: L{Element}
         @raise StopIterator: At the end.
+
         """
         try:
             child = self.children[self.pos]
             self.pos += 1
             return child
-        except:
+        except Exception:
             raise StopIteration()
 
 
 class PrefixNormalizer:
     """
     The prefix normalizer provides namespace prefix normalization.
+
     @ivar node: A node to normalize.
     @type node: L{Element}
     @ivar branch: The nodes flattened branch.
-    @type branch: [L{Element},..]
+    @type branch: [L{Element},...]
     @ivar namespaces: A unique list of namespaces (URI).
-    @type namespaces: [str,]
+    @type namespaces: [str,...]
     @ivar prefixes: A reverse dict of prefixes.
-    @type prefixes: {u, p}
+    @type prefixes: {u: p}
+
     """
 
     @classmethod
     def apply(cls, node):
         """
         Normalize the specified node.
+
         @param node: A node to normalize.
         @type node: L{Element}
         @return: The normalized node.
         @rtype: L{Element}
+
         """
-        pn = PrefixNormalizer(node)
-        return pn.refit()
+        return PrefixNormalizer(node).refit()
 
     def __init__(self, node):
         """
         @param node: A node to normalize.
         @type node: L{Element}
+
         """
         self.node = node
         self.branch = node.branch()
@@ -980,8 +1070,10 @@ class PrefixNormalizer:
     def getNamespaces(self):
         """
         Get the I{unique} set of namespaces referenced in the branch.
+
         @return: A set of namespaces.
         @rtype: set
+
         """
         s = set()
         for n in self.branch + self.node.ancestors():
@@ -993,10 +1085,12 @@ class PrefixNormalizer:
     def pset(self, n):
         """
         Convert the nodes nsprefixes into a set.
+
         @param n: A node.
         @type n: L{Element}
         @return: A set of namespaces.
         @rtype: set
+
         """
         s = set()
         for ns in n.nsprefixes.items():
@@ -1007,28 +1101,25 @@ class PrefixNormalizer:
     def genPrefixes(self):
         """
         Generate a I{reverse} mapping of unique prefixes for all namespaces.
-        @return: A referse dict of prefixes.
-        @rtype: {u, p}
+
+        @return: A reverse dict of prefixes.
+        @rtype: {u: p}
+
         """
         prefixes = {}
         n = 0
         for u in self.namespaces:
-            p = 'ns%d' % n
-            prefixes[u] = p
+            prefixes[u] = "ns%d" % (n,)
             n += 1
         return prefixes
 
     def refit(self):
-        """
-        Refit (normalize) the prefixes in the node.
-        """
+        """Refit (normalize) the prefixes in the node."""
         self.refitNodes()
         self.refitMappings()
 
     def refitNodes(self):
-        """
-        Refit (normalize) all of the nodes in the branch.
-        """
+        """Refit (normalize) all of the nodes in the branch."""
         for n in self.branch:
             if n.prefix is not None:
                 ns = n.namespace()
@@ -1039,8 +1130,10 @@ class PrefixNormalizer:
     def refitAttrs(self, n):
         """
         Refit (normalize) all of the attributes in the node.
+
         @param n: A node.
         @type n: L{Element}
+
         """
         for a in n.attributes:
             self.refitAddr(a)
@@ -1048,8 +1141,10 @@ class PrefixNormalizer:
     def refitAddr(self, a):
         """
         Refit (normalize) the attribute.
+
         @param a: An attribute.
         @type a: L{Attribute}
+
         """
         if a.prefix is not None:
             ns = a.namespace()
@@ -1060,21 +1155,21 @@ class PrefixNormalizer:
     def refitValue(self, a):
         """
         Refit (normalize) the attribute's value.
+
         @param a: An attribute.
         @type a: L{Attribute}
+
         """
-        p,name = splitPrefix(a.getValue())
-        if p is None: return
+        p, name = splitPrefix(a.getValue())
+        if p is None:
+            return
         ns = a.resolvePrefix(p)
         if self.permit(ns):
-            u = ns[1]
-            p = self.prefixes[u]
-            a.setValue(':'.join((p, name)))
+            p = self.prefixes[ns[1]]
+            a.setValue(":".join((p, name)))
 
     def refitMappings(self):
-        """
-        Refit (normalize) all of the nsprefix mappings.
-        """
+        """Refit (normalize) all of the nsprefix mappings."""
         for n in self.branch:
             n.nsprefixes = {}
         n = self.node
@@ -1084,23 +1179,27 @@ class PrefixNormalizer:
     def permit(self, ns):
         """
         Get whether the I{ns} is to be normalized.
+
         @param ns: A namespace.
-        @type ns: (p,u)
+        @type ns: (p, u)
         @return: True if to be included.
         @rtype: boolean
+
         """
         return not self.skip(ns)
 
     def skip(self, ns):
         """
         Get whether the I{ns} is to B{not} be normalized.
+
         @param ns: A namespace.
-        @type ns: (p,u)
+        @type ns: (p, u)
         @return: True if to be skipped.
         @rtype: boolean
+
         """
-        return (ns is None or
-            ns == Namespace.default or
-            ns == Namespace.xsdns or
-            ns == Namespace.xsins or
-            ns == Namespace.xmlns)
+        return ns is None or ns in (
+            Namespace.default,
+            Namespace.xsdns,
+            Namespace.xsins,
+            Namespace.xmlns)
diff --git a/suds/sax/enc.py b/suds/sax/enc.py
index 8d3219c..f750556 100644
--- a/suds/sax/enc.py
+++ b/suds/sax/enc.py
@@ -1,66 +1,64 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Provides XML I{special character} encoder classes.
+
 """
 
 import re
 
+
 class Encoder:
     """
     An XML special character encoder/decoder.
+
     @cvar encodings: A mapping of special characters encoding.
-    @type encodings: [(str,str)]
+    @type encodings: [(str, str),...]
     @cvar decodings: A mapping of special characters decoding.
-    @type decodings: [(str,str)]
-    @cvar special: A list of special characters
-    @type special: [char]
+    @type decodings: [(str, str),...]
+    @cvar special: A list of special characters.
+    @type special: [char,...]
+
     """
 
-    encodings = \
-        (( '&(?!(amp|lt|gt|quot|apos);)', '&' ),( '<', '<' ),( '>', '>' ),( '"', '"' ),("'", ''' ))
-    decodings = \
-        (( '<', '<' ),( '>', '>' ),( '"', '"' ),( ''', "'" ),( '&', '&' ))
-    special = \
-        ('&', '<', '>', '"', "'")
-
-    def needsEncoding(self, s):
-        """
-        Get whether string I{s} contains special characters.
-        @param s: A string to check.
-        @type s: str
-        @return: True if needs encoding.
-        @rtype: boolean
-        """
-        if isinstance(s, basestring):
-            for c in self.special:
-                if c in s:
-                    return True
-        return False
+    encodings = (
+        ("&(?!(amp|lt|gt|quot|apos);)", "&"),
+        ("<", "<"),
+        (">", ">"),
+        ('"', """),
+        ("'", "'"))
+    decodings = (
+        ("<", "<"),
+        (">", ">"),
+        (""", '"'),
+        ("'", "'"),
+        ("&", "&"))
+    special = ("&", "<", ">", '"', "'")
 
     def encode(self, s):
         """
         Encode special characters found in string I{s}.
+
         @param s: A string to encode.
         @type s: str
         @return: The encoded string.
         @rtype: str
+
         """
-        if isinstance(s, basestring) and self.needsEncoding(s):
+        if isinstance(s, basestring) and self.__needs_encoding(s):
             for x in self.encodings:
                 s = re.sub(x[0], x[1], s)
         return s
@@ -68,12 +66,29 @@ class Encoder:
     def decode(self, s):
         """
         Decode special characters encodings found in string I{s}.
+
         @param s: A string to decode.
         @type s: str
         @return: The decoded string.
         @rtype: str
+
         """
-        if isinstance(s, basestring) and '&' in s:
+        if isinstance(s, basestring) and "&" in s:
             for x in self.decodings:
                 s = s.replace(x[0], x[1])
         return s
+
+    def __needs_encoding(self, s):
+        """
+        Get whether string I{s} contains special characters.
+
+        @param s: A string to check.
+        @type s: str
+        @return: True if needs encoding.
+        @rtype: boolean
+
+        """
+        if isinstance(s, basestring):
+            for c in self.special:
+                if c in s:
+                    return True
diff --git a/suds/sax/parser.py b/suds/sax/parser.py
index a82583a..d532c7f 100644
--- a/suds/sax/parser.py
+++ b/suds/sax/parser.py
@@ -1,28 +1,27 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The sax module contains a collection of classes that provide a
-(D)ocument (O)bject (M)odel representation of an XML document.
-The goal is to provide an easy, intuitive interface for managing XML
-documents.  Although, the term, DOM, is used above, this model is
-B{far} better.
+Classes providing a (D)ocument (O)bject (M)odel representation of an XML
+document.
 
-XML namespaces in suds are represented using a (2) element tuple
-containing the prefix and the URI.  Eg: I{('tns', 'http://myns')}
+The goal is to provide an easy, intuitive interface for managing XML documents.
+Although the term DOM is used above, this model is B{far} better.
+
+XML namespaces in suds are represented using a (2) element tuple containing the
+prefix and the URI, e.g. I{('tns', 'http://myns')}.
 
 """
 
@@ -40,7 +39,7 @@ from xml.sax.handler import feature_external_ges
 
 
 class Handler(ContentHandler):
-    """ sax hanlder """
+    """SAX handler."""
 
     def __init__(self):
         self.nodes = [Document()]
@@ -51,7 +50,7 @@ class Handler(ContentHandler):
         for a in attrs.getNames():
             n = unicode(a)
             v = unicode(attrs.getValue(a))
-            attribute = Attribute(n,v)
+            attribute = Attribute(n, v)
             if self.mapPrefix(node, attribute):
                 continue
             node.append(attribute)
@@ -60,29 +59,26 @@ class Handler(ContentHandler):
         self.push(node)
 
     def mapPrefix(self, node, attribute):
-        skip = False
-        if attribute.name == 'xmlns':
+        if attribute.name == "xmlns":
             if len(attribute.value):
                 node.expns = unicode(attribute.value)
-            skip = True
-        elif attribute.prefix == 'xmlns':
+            return True
+        if attribute.prefix == "xmlns":
             prefix = attribute.name
             node.nsprefixes[prefix] = unicode(attribute.value)
-            skip = True
-        return skip
+            return True
+        return False
 
     def endElement(self, name):
         name = unicode(name)
-        current = self.top()
-        if len(current.charbuffer):
-            current.text = Text(u''.join(current.charbuffer))
+        current = self.pop()
+        if name != current.qname():
+            raise Exception("malformed document")
+        if current.charbuffer:
+            current.text = Text(u"".join(current.charbuffer))
         del current.charbuffer
-        if len(current):
+        if current:
             current.trim()
-        if name == current.qname():
-            self.pop()
-        else:
-            raise Exception('malformed document')
 
     def characters(self, content):
         text = unicode(content)
@@ -97,11 +93,11 @@ class Handler(ContentHandler):
         return self.nodes.pop()
 
     def top(self):
-        return self.nodes[len(self.nodes)-1]
+        return self.nodes[-1]
 
 
 class Parser:
-    """ SAX Parser """
+    """SAX parser."""
 
     @classmethod
     def saxparser(cls):
@@ -109,28 +105,33 @@ class Parser:
         p.setFeature(feature_external_ges, 0)
         h = Handler()
         p.setContentHandler(h)
-        return (p, h)
+        return p, h
 
     def parse(self, file=None, string=None):
         """
         SAX parse XML text.
+
         @param file: Parse a python I{file-like} object.
-        @type file: I{file-like} object.
+        @type file: I{file-like} object
         @param string: Parse string XML.
         @type string: str
+        @return: Parsed XML document.
+        @rtype: L{Document}
+
         """
+        if file is None and string is None:
+            return
         timer = suds.metrics.Timer()
         timer.start()
-        sax, handler = self.saxparser()
-        if file is not None:
-            sax.parse(file)
-            timer.stop()
-            suds.metrics.log.debug('sax (%s) duration: %s', file, timer)
-            return handler.nodes[0]
-        if string is not None:
+        source = file
+        if file is None:
             source = InputSource(None)
             source.setByteStream(suds.BytesIO(string))
-            sax.parse(source)
-            timer.stop()
-            suds.metrics.log.debug('%s\nsax duration: %s', string, timer)
-            return handler.nodes[0]
+        sax, handler = self.saxparser()
+        sax.parse(source)
+        timer.stop()
+        if file is None:
+            suds.metrics.log.debug("%s\nsax duration: %s", string, timer)
+        else:
+            suds.metrics.log.debug("sax (%s) duration: %s", file, timer)
+        return handler.nodes[0]
diff --git a/suds/servicedefinition.py b/suds/servicedefinition.py
index 6b0e72f..51f95c6 100644
--- a/suds/servicedefinition.py
+++ b/suds/servicedefinition.py
@@ -222,7 +222,7 @@ class ServiceDefinition(UnicodeMixin):
                 sig.append(')')
                 try:
                     s.append(''.join(sig))
-                except:
+                except Exception:
                     pass
             s.append(indent(3))
             s.append('Types (%d):' % len(self.types))
diff --git a/suds/store.py b/suds/store.py
index e5931aa..156e022 100644
--- a/suds/store.py
+++ b/suds/store.py
@@ -1,23 +1,22 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Support for holding XML document texts that may then be accessed internally by
-suds without having to download them from an external source. Also contains XML
-document content to be distributed alongside the suds library.
+Support for holding XML document content that may then be accessed internally
+by suds without having to download them from an external source. Also contains
+XML document content to be distributed alongside the suds library.
 
 """
 
@@ -527,33 +526,33 @@ soap5_encoding_schema = suds.byte_str("""\
 """)
 
 
-class DocumentStore:
+class DocumentStore(object):
     """
-    The I{suds} document store provides a local repository for XML documents.
+    Local XML document content repository.
+
+    Each XML document is identified by its location, i.e. URL without any
+    protocol identifier. Contained XML documents can be looked up using any URL
+    referencing that same location.
 
-    @cvar protocol: The URL protocol for the store.
-    @type protocol: str
-    @cvar store: The mapping of URL location to documents.
-    @type store: dict
     """
 
     def __init__(self, *args, **kwargs):
         self.__store = {
-            'schemas.xmlsoap.org/soap/encoding/':soap5_encoding_schema}
+            'schemas.xmlsoap.org/soap/encoding/': soap5_encoding_schema}
         self.update = self.__store.update
         self.update(*args, **kwargs)
 
     def __len__(self):
-        # Implementation note:
-        #   We can not implement '__len__' as simply self.__store.__len__, as
-        # we do for 'update' because that causes py2to3 conversion to fail.
-        #                                            (08.05.2013.) (Jurko)
         return len(self.__store)
 
     def open(self, url):
         """
         Open a document at the specified URL.
 
+        The document URL's needs not contain a protocol identifier, and if it
+        does, that protocol identifier is ignored when looking up the store
+        content.
+
         Missing documents referenced using the internal 'suds' protocol are
         reported by raising an exception. For other protocols, None is returned
         instead.
@@ -562,6 +561,7 @@ class DocumentStore:
         @type url: str
         @return: Document content or None if not found.
         @rtype: bytes
+
         """
         protocol, location = self.__split(url)
         content = self.__find(location)
@@ -572,20 +572,24 @@ class DocumentStore:
     def __find(self, location):
         """
         Find the specified location in the store.
+
         @param location: The I{location} part of a URL.
         @type location: str
         @return: Document content or None if not found.
         @rtype: bytes
+
         """
         return self.__store.get(location)
 
     def __split(self, url):
         """
-        Split the URL into I{protocol} and I{location}
+        Split the given URL into its I{protocol} & I{location} components.
+
         @param url: A URL.
         @param url: str
-        @return: (I{url}, I{location})
-        @rtype: tuple
+        @return: (I{protocol}, I{location})
+        @rtype: (str, str)
+
         """
         parts = url.split('://', 1)
         if len(parts) == 2:
diff --git a/suds/sudsobject.py b/suds/sudsobject.py
index f96d419..0c18d5a 100644
--- a/suds/sudsobject.py
+++ b/suds/sudsobject.py
@@ -1,23 +1,22 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{sudsobject} module provides a collection of suds objects
-that are primarily used for the highly dynamic interactions with
-wsdl/xsd defined types.
+Provides a collection of suds objects primarily used for highly dynamic
+interactions with WSDL/XSD defined types.
+
 """
 
 from suds import *
@@ -28,12 +27,15 @@ log = getLogger(__name__)
 
 def items(sobject):
     """
-    Extract the I{items} from a suds object much like the
-    items() method works on I{dict}.
+    Extract the I{items} from a suds object.
+
+    Much like the items() method works on I{dict}.
+
     @param sobject: A suds object
     @type sobject: L{Object}
     @return: A list of items contained in I{sobject}.
     @rtype: [(key, value),...]
+
     """
     for item in sobject:
         yield item
@@ -42,21 +44,24 @@ def items(sobject):
 def asdict(sobject):
     """
     Convert a sudsobject into a dictionary.
+
     @param sobject: A suds object
     @type sobject: L{Object}
-    @return: A python dictionary containing the
-        items contained in I{sobject}.
+    @return: A python dictionary containing the items contained in I{sobject}.
     @rtype: dict
+
     """
     return dict(items(sobject))
 
 def merge(a, b):
     """
     Merge all attributes and metadata from I{a} to I{b}.
+
     @param a: A I{source} object
     @type a: L{Object}
     @param b: A I{destination} object
     @type b: L{Object}
+
     """
     for item in a:
         setattr(b, item[0], item[1])
@@ -66,23 +71,29 @@ def merge(a, b):
 def footprint(sobject):
     """
     Get the I{virtual footprint} of the object.
-    This is really a count of the attributes in the branch with a significant value.
+
+    This is really a count of all the significant value attributes in the
+    branch.
+
     @param sobject: A suds object.
     @type sobject: L{Object}
     @return: The branch footprint.
     @rtype: int
+
     """
     n = 0
     for a in sobject.__keylist__:
         v = getattr(sobject, a)
-        if v is None: continue
+        if v is None:
+            continue
         if isinstance(v, Object):
             n += footprint(v)
             continue
-        if hasattr(v, '__len__'):
-            if len(v): n += 1
+        if hasattr(v, "__len__"):
+            if len(v):
+                n += 1
             continue
-        n +=1
+        n += 1
     return n
 
 
@@ -94,9 +105,9 @@ class Factory:
     def subclass(cls, name, bases, dict={}):
         if not isinstance(bases, tuple):
             bases = (bases,)
-        # name is type unicode in python 2 -> not accepted by the type()
+        # name is of type unicode in python 2 -> not accepted by type()
         name = str(name)
-        key = '.'.join((name, str(bases)))
+        key = ".".join((name, str(bases)))
         subclass = cls.cache.get(key)
         if subclass is None:
             subclass = type(name, bases, dict)
@@ -132,19 +143,18 @@ class Object(UnicodeMixin):
         self.__metadata__ = Metadata()
 
     def __setattr__(self, name, value):
-        builtin = name.startswith('__') and name.endswith('__')
-        if not builtin and \
-            name not in self.__keylist__:
+        builtin = name.startswith("__") and name.endswith("__")
+        if not builtin and name not in self.__keylist__:
             self.__keylist__.append(name)
         self.__dict__[name] = value
 
     def __delattr__(self, name):
         try:
             del self.__dict__[name]
-            builtin = name.startswith('__') and name.endswith('__')
+            builtin = name.startswith("__") and name.endswith("__")
             if not builtin:
                 self.__keylist__.remove(name)
-        except:
+        except Exception:
             cls = self.__class__.__name__
             raise AttributeError, "%s has no attribute '%s'" % (cls, name)
 
@@ -197,13 +207,11 @@ class Iter:
             ordering = sobject.__metadata__.ordering
             ordered = set(ordering)
             if not ordered.issuperset(keyset):
-                log.debug(
-                    '%s must be superset of %s, ordering ignored',
-                    keylist,
-                    ordering)
+                log.debug("%s must be superset of %s, ordering ignored",
+                    keylist, ordering)
                 raise KeyError()
             return ordering
-        except:
+        except Exception:
             return keylist
 
     def __iter__(self):
@@ -231,7 +239,7 @@ class Property(Object):
 
     def items(self):
         for item in self:
-            if item[0] != 'value':
+            if item[0] != "value":
                 yield item
 
     def get(self):
@@ -243,141 +251,141 @@ class Property(Object):
 
 
 class Printer:
-    """
-    Pretty printing of a Object object.
-    """
+    """Pretty printing of a Object object."""
 
     @classmethod
-    def indent(cls, n): return '%*s'%(n*3,' ')
+    def indent(cls, n):
+        return "%*s" % (n * 3, " ")
 
     def tostr(self, object, indent=-2):
-        """ get s string representation of object """
+        """Get s string representation of object."""
         history = []
         return self.process(object, history, indent)
 
     def process(self, object, h, n=0, nl=False):
-        """ print object using the specified indent (n) and newline (nl). """
+        """Print object using the specified indent (n) and newline (nl)."""
         if object is None:
-            return 'None'
+            return "None"
         if isinstance(object, Object):
             if len(object) == 0:
-                return '<empty>'
-            return self.print_object(object, h, n+2, nl)
+                return "<empty>"
+            return self.print_object(object, h, n + 2, nl)
         if isinstance(object, dict):
             if len(object) == 0:
-                return '<empty>'
-            return self.print_dictionary(object, h, n+2, nl)
-        if isinstance(object, (list,tuple)):
+                return "<empty>"
+            return self.print_dictionary(object, h, n + 2, nl)
+        if isinstance(object, (list, tuple)):
             if len(object) == 0:
-                return '<empty>'
-            return self.print_collection(object, h, n+2)
+                return "<empty>"
+            return self.print_collection(object, h, n + 2)
         if isinstance(object, basestring):
-            return '"%s"' % tostr(object)
-        return '%s' % tostr(object)
+            return '"%s"' % (tostr(object),)
+        return "%s" % (tostr(object),)
 
     def print_object(self, d, h, n, nl=False):
-        """ print complex using the specified indent (n) and newline (nl). """
+        """Print complex using the specified indent (n) and newline (nl)."""
         s = []
         cls = d.__class__
         if d in h:
-            s.append('(')
+            s.append("(")
             s.append(cls.__name__)
-            s.append(')')
-            s.append('...')
-            return ''.join(s)
+            s.append(")")
+            s.append("...")
+            return "".join(s)
         h.append(d)
         if nl:
-            s.append('\n')
+            s.append("\n")
             s.append(self.indent(n))
         if cls != Object:
-            s.append('(')
+            s.append("(")
             if isinstance(d, Facade):
                 s.append(d.__metadata__.facade)
             else:
                 s.append(cls.__name__)
-            s.append(')')
-        s.append('{')
+            s.append(")")
+        s.append("{")
         for item in d:
             if self.exclude(d, item):
                 continue
             item = self.unwrap(d, item)
-            s.append('\n')
+            s.append("\n")
             s.append(self.indent(n+1))
             if isinstance(item[1], (list,tuple)):
                 s.append(item[0])
-                s.append('[]')
+                s.append("[]")
             else:
                 s.append(item[0])
-            s.append(' = ')
+            s.append(" = ")
             s.append(self.process(item[1], h, n, True))
-        s.append('\n')
+        s.append("\n")
         s.append(self.indent(n))
-        s.append('}')
+        s.append("}")
         h.pop()
-        return ''.join(s)
+        return "".join(s)
 
     def print_dictionary(self, d, h, n, nl=False):
-        """ print complex using the specified indent (n) and newline (nl). """
-        if d in h: return '{}...'
+        """Print complex using the specified indent (n) and newline (nl)."""
+        if d in h:
+            return "{}..."
         h.append(d)
         s = []
         if nl:
-            s.append('\n')
+            s.append("\n")
             s.append(self.indent(n))
-        s.append('{')
+        s.append("{")
         for item in d.items():
-            s.append('\n')
+            s.append("\n")
             s.append(self.indent(n+1))
             if isinstance(item[1], (list,tuple)):
                 s.append(tostr(item[0]))
-                s.append('[]')
+                s.append("[]")
             else:
                 s.append(tostr(item[0]))
-            s.append(' = ')
+            s.append(" = ")
             s.append(self.process(item[1], h, n, True))
-        s.append('\n')
+        s.append("\n")
         s.append(self.indent(n))
-        s.append('}')
+        s.append("}")
         h.pop()
-        return ''.join(s)
+        return "".join(s)
 
     def print_collection(self, c, h, n):
-        """ print collection using the specified indent (n) and newline (nl). """
-        if c in h: return '[]...'
+        """Print collection using the specified indent (n) and newline (nl)."""
+        if c in h:
+            return "[]..."
         h.append(c)
         s = []
         for item in c:
-            s.append('\n')
+            s.append("\n")
             s.append(self.indent(n))
-            s.append(self.process(item, h, n-2))
-            s.append(',')
+            s.append(self.process(item, h, n - 2))
+            s.append(",")
         h.pop()
-        return ''.join(s)
+        return "".join(s)
 
     def unwrap(self, d, item):
-        """ translate (unwrap) using an optional wrapper function """
-        nopt = lambda x: x
+        """Translate (unwrap) using an optional wrapper function."""
         try:
             md = d.__metadata__
-            pmd = getattr(md, '__print__', None)
+            pmd = getattr(md, "__print__", None)
             if pmd is None:
                 return item
-            wrappers = getattr(pmd, 'wrappers', {})
-            fn = wrappers.get(item[0], nopt)
+            wrappers = getattr(pmd, "wrappers", {})
+            fn = wrappers.get(item[0], lambda x: x)
             return (item[0], fn(item[1]))
-        except:
+        except Exception:
             pass
         return item
 
     def exclude(self, d, item):
-        """ check metadata for excluded items """
+        """Check metadata for excluded items."""
         try:
             md = d.__metadata__
-            pmd = getattr(md, '__print__', None)
+            pmd = getattr(md, "__print__", None)
             if pmd is None:
                 return False
-            excludes = getattr(pmd, 'excludes', [])
-            return ( item[0] in excludes )
-        except:
+            excludes = getattr(pmd, "excludes", [])
+            return item[0] in excludes
+        except Exception:
             pass
         return False
diff --git a/suds/transport/__init__.py b/suds/transport/__init__.py
index b193aea..0ed4e4e 100644
--- a/suds/transport/__init__.py
+++ b/suds/transport/__init__.py
@@ -20,6 +20,8 @@ Contains transport interface (classes).
 
 from suds import UnicodeMixin
 
+import sys
+
 
 class TransportError(Exception):
     def __init__(self, reason, httpcode, fp=None):
@@ -32,10 +34,16 @@ class Request(UnicodeMixin):
     """
     A transport request.
 
+    Request URL input data may be given as either a byte or a unicode string,
+    but it may not under any circumstances contain non-ASCII characters. The
+    URL value is stored as a str value internally. With Python versions prior
+    to 3.0, str is the byte string type, while with later Python versions it is
+    the unicode string type.
+
     @ivar url: The URL for the request.
     @type url: str
-    @ivar message: The message to be sent in a POST request.
-    @type message: str
+    @ivar message: The optional message to be sent in the request body.
+    @type message: bytes|None
     @ivar headers: The HTTP headers to be used for the request.
     @type headers: dict
 
@@ -43,22 +51,43 @@ class Request(UnicodeMixin):
 
     def __init__(self, url, message=None):
         """
+        Raised exception in case of detected non-ASCII URL characters may be
+        either UnicodeEncodeError or UnicodeDecodeError, depending on the used
+        Python version's str type and the exact value passed as URL input data.
+
         @param url: The URL for the request.
-        @type url: str
-        @param message: The (optional) message to be sent in the request.
-        @type message: str
+        @type url: bytes|str|unicode
+        @param message: The optional message to be sent in the request body.
+        @type message: bytes|None
 
         """
-        self.url = url
+        self.__set_URL(url)
         self.headers = {}
         self.message = message
 
     def __unicode__(self):
-        return u"""\
-URL: %s
-HEADERS: %s
-MESSAGE:
-%s""" % (self.url, self.headers, self.message)
+        result = [u"URL: %s\nHEADERS: %s" % (self.url, self.headers)]
+        if self.message is not None:
+            result.append(u"MESSAGE:")
+            result.append(self.message.decode("raw_unicode_escape"))
+        return u"\n".join(result)
+
+    def __set_URL(self, url):
+        """
+        URL is stored as a str internally and must not contain ASCII chars.
+
+        Raised exception in case of detected non-ASCII URL characters may be
+        either UnicodeEncodeError or UnicodeDecodeError, depending on the used
+        Python version's str type and the exact value passed as URL input data.
+
+        """
+        if isinstance(url, str):
+            url.encode("ascii")  # Check for non-ASCII characters.
+            self.url = url
+        elif sys.version_info < (3, 0):
+            self.url = url.encode("ascii")
+        else:
+            self.url = url.decode("ascii")
 
 
 class Reply(UnicodeMixin):
@@ -67,10 +96,10 @@ class Reply(UnicodeMixin):
 
     @ivar code: The HTTP code returned.
     @type code: int
-    @ivar message: The message to be sent in a POST request.
-    @type message: str
-    @ivar headers: The HTTP headers to be used for the request.
+    @ivar headers: The HTTP headers included in the received reply.
     @type headers: dict
+    @ivar message: The message received as a reply.
+    @type message: bytes
 
     """
 
@@ -78,10 +107,10 @@ class Reply(UnicodeMixin):
         """
         @param code: The HTTP code returned.
         @type code: int
-        @param headers: The HTTP returned headers.
+        @param headers: The HTTP headers included in the received reply.
         @type headers: dict
-        @param message: The (optional) reply message received.
-        @type message: str
+        @param message: The (optional) message received as a reply.
+        @type message: bytes
 
         """
         self.code = code
@@ -93,10 +122,10 @@ class Reply(UnicodeMixin):
 CODE: %s
 HEADERS: %s
 MESSAGE:
-%s""" % (self.code, self.headers, self.message)
+%s""" % (self.code, self.headers, self.message.decode("raw_unicode_escape"))
 
 
-class Transport:
+class Transport(object):
     """The transport I{interface}."""
 
     def __init__(self):
@@ -118,7 +147,7 @@ class Transport:
 
     def send(self, request):
         """
-        Send soap message. Implementations are expected to handle:
+        Send SOAP message. Implementations are expected to handle:
             - proxies
             - I{HTTP} headers
             - cookies
diff --git a/suds/transport/http.py b/suds/transport/http.py
index e3d042b..4dbfbe4 100644
--- a/suds/transport/http.py
+++ b/suds/transport/http.py
@@ -14,7 +14,7 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains classes for basic HTTP transport implementations.
+Basic HTTP transport implementation classes.
 
 """
 
@@ -27,7 +27,6 @@ import httplib
 import socket
 import sys
 import urllib2
-from urlparse import urlparse
 
 from logging import getLogger
 log = getLogger(__name__)
@@ -43,11 +42,11 @@ class HttpTransport(Transport):
     def __init__(self, **kwargs):
         """
         @param kwargs: Keyword arguments.
-            - B{proxy} - An http proxy to be specified on requests.
+            - B{proxy} - An HTTP proxy to be specified on requests.
                  The proxy is defined as {protocol:proxy,}
                     - type: I{dict}
                     - default: {}
-            - B{timeout} - Set the url open timeout (seconds).
+            - B{timeout} - Set the URL open timeout (seconds).
                     - type: I{float}
                     - default: 90
 
@@ -60,7 +59,7 @@ class HttpTransport(Transport):
 
     def open(self, request):
         try:
-            url = self.__get_request_url(request)
+            url = self.__get_request_url_for_urllib(request)
             log.debug('opening (%s)', url)
             u2request = urllib2.Request(url)
             self.proxy = self.options.proxy
@@ -69,8 +68,7 @@ class HttpTransport(Transport):
             raise TransportError(str(e), e.code, e.fp)
 
     def send(self, request):
-        result = None
-        url = self.__get_request_url(request)
+        url = self.__get_request_url_for_urllib(request)
         msg = request.message
         headers = request.headers
         try:
@@ -81,18 +79,15 @@ class HttpTransport(Transport):
             log.debug('sending:\n%s', request)
             fp = self.u2open(u2request)
             self.getcookies(fp, u2request)
+            headers = fp.headers
             if sys.version_info < (3, 0):
-                headers = fp.headers.dict
-            else:
-                headers = fp.headers
-            result = Reply(httplib.OK, headers, fp.read())
-            log.debug('received:\n%s', result)
+                headers = headers.dict
+            reply = Reply(httplib.OK, headers, fp.read())
+            log.debug('received:\n%s', reply)
+            return reply
         except urllib2.HTTPError, e:
-            if e.code in (httplib.ACCEPTED, httplib.NO_CONTENT):
-                result = None
-            else:
+            if e.code not in (httplib.ACCEPTED, httplib.NO_CONTENT):
                 raise TransportError(e.msg, e.code, e.fp)
-        return result
 
     def addcookies(self, u2request):
         """
@@ -151,9 +146,7 @@ class HttpTransport(Transport):
         @rtype: [Handler,...]
 
         """
-        handlers = []
-        handlers.append(urllib2.ProxyHandler(self.proxy))
-        return handlers
+        return [urllib2.ProxyHandler(self.proxy)]
 
     def u2ver(self):
         """
@@ -161,6 +154,7 @@ class HttpTransport(Transport):
 
         @return: The urllib2 version.
         @rtype: float
+
         """
         try:
             part = urllib2.__version__.split('.', 1)
@@ -177,17 +171,15 @@ class HttpTransport(Transport):
         return clone
 
     @staticmethod
-    def __get_request_url(request):
+    def __get_request_url_for_urllib(request):
         """
         Returns the given request's URL, properly encoded for use with urllib.
 
-        URLs are allowed to be:
-            under Python 2.x: unicode strings, single-byte strings;
-            under Python 3.x: unicode strings.
-        In any case, they are allowed to contain ASCII characters only. We
-        raise a UnicodeError derived exception if they contain any non-ASCII
-        characters (UnicodeEncodeError or UnicodeDecodeError depending on
-        whether the URL was specified as a unicode or a single-byte string).
+        We expect that the given request object already verified that the URL
+        contains ASCII characters only and stored it as a native str value.
+
+        urllib accepts URL information as a native str value and may break
+        unexpectedly if given URL information in another format.
 
         Python 3.x httplib.client implementation must be given a unicode string
         and not a bytes object and the given string is internally converted to
@@ -206,21 +198,8 @@ class HttpTransport(Transport):
         unicode.
 
         """
-        url = request.url
-        py2 = sys.version_info < (3, 0)
-        if py2 and isinstance(url, str):
-            encodedURL = url
-            decodedURL = url.decode("ascii")
-        else:
-            # On Python3, calling encode() on a bytes or a bytearray object
-            # raises an AttributeError exception.
-            assert py2 or not isinstance(url, bytes)
-            assert py2 or not isinstance(url, bytearray)
-            decodedURL = url
-            encodedURL = url.encode("ascii")
-        if py2:
-            return encodedURL  # Python 2 urllib - single-byte URL string.
-        return decodedURL  # Python 3 urllib - unicode URL string.
+        assert isinstance(request.url, str)
+        return request.url
 
 
 class HttpAuthenticated(HttpTransport):
@@ -228,6 +207,7 @@ class HttpAuthenticated(HttpTransport):
     Provides basic HTTP authentication for servers that do not follow the
     specified challenge/response model. Appends the I{Authorization} HTTP
     header with base64 encoded credentials on every HTTP request.
+
     """
 
     def open(self, request):
@@ -240,15 +220,14 @@ class HttpAuthenticated(HttpTransport):
 
     def addcredentials(self, request):
         credentials = self.credentials()
-        if not (None in credentials):
+        if None not in credentials:
             credentials = ':'.join(credentials)
-            if sys.version_info < (3,0):
-                basic = 'Basic %s' % base64.b64encode(credentials)
+            if sys.version_info < (3, 0):
+                encodedString = base64.b64encode(credentials)
             else:
                 encodedBytes = base64.urlsafe_b64encode(credentials.encode())
                 encodedString = encodedBytes.decode()
-                basic = 'Basic %s' % encodedString
-            request.headers['Authorization'] = basic
+            request.headers['Authorization'] = 'Basic %s' % encodedString
 
     def credentials(self):
         return self.options.username, self.options.password
diff --git a/suds/version.py b/suds/version.py
index c2367b2..8c52147 100644
--- a/suds/version.py
+++ b/suds/version.py
@@ -22,5 +22,5 @@ See the setup.py script for more detailed information.
 
 """
 
-__version__ = "0.6"
+__version__ = "0.7.dev0"
 __build__ = ""
diff --git a/suds/wsdl.py b/suds/wsdl.py
index 987dbc3..23f0126 100644
--- a/suds/wsdl.py
+++ b/suds/wsdl.py
@@ -1,34 +1,35 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 The I{wsdl} module provides an objectification of the WSDL.
-The primary class is I{Definitions} as it represents the root element
-found in the document.
+
+The primary class is I{Definitions}, representing the root element found in a
+WSDL schema document.
+
 """
 
 from suds import *
-from suds.sax.element import Element
 from suds.bindings.document import Document
 from suds.bindings.rpc import RPC, Encoded
+from suds.reader import DocumentReader
+from suds.sax.element import Element
+from suds.sudsobject import Object, Facade, Metadata
 from suds.xsd import qualify, Namespace
-from suds.xsd.schema import Schema, SchemaCollection
 from suds.xsd.query import ElementQuery
-from suds.sudsobject import Object, Facade, Metadata
-from suds.reader import DocumentReader
+from suds.xsd.schema import Schema, SchemaCollection
 
 import re
 import soaparray
@@ -39,34 +40,39 @@ log = getLogger(__name__)
 
 
 wsdlns = (None, "http://schemas.xmlsoap.org/wsdl/")
-soapns = (None, 'http://schemas.xmlsoap.org/wsdl/soap/')
-soap12ns = (None, 'http://schemas.xmlsoap.org/wsdl/soap12/')
+soapns = (None, "http://schemas.xmlsoap.org/wsdl/soap/")
+soap12ns = (None, "http://schemas.xmlsoap.org/wsdl/soap12/")
 
 
 class WObject(Object):
     """
     Base object for WSDL types.
+
     @ivar root: The XML I{root} element.
     @type root: L{Element}
+
     """
 
     def __init__(self, root):
         """
         @param root: An XML root element.
         @type root: L{Element}
+
         """
         Object.__init__(self)
         self.root = root
         pmd = Metadata()
-        pmd.excludes = ['root']
+        pmd.excludes = ["root"]
         pmd.wrappers = dict(qname=repr)
         self.__metadata__.__print__ = pmd
 
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         pass
 
@@ -74,10 +80,12 @@ class WObject(Object):
 class NamedObject(WObject):
     """
     A B{named} WSDL object.
+
     @ivar name: The name of the object.
     @type name: str
     @ivar qname: The I{qualified} name of the object.
     @type qname: (name, I{namespace-uri}).
+
     """
 
     def __init__(self, root, definitions):
@@ -86,18 +94,19 @@ class NamedObject(WObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         WObject.__init__(self, root)
-        self.name = root.get('name')
+        self.name = root.get("name")
         self.qname = (self.name, definitions.tns[1])
         pmd = self.__metadata__.__print__
-        pmd.wrappers['qname'] = repr
+        pmd.wrappers["qname"] = repr
 
 
 class Definitions(WObject):
     """
-    I{root} container for all the WSDL objects as defined by
-    <wsdl:definitions/>
+    I{Root} container for all the WSDL objects defined by <wsdl:definitions/>.
+
     @ivar id: The object id.
     @type id: str
     @ivar options: An options dictionary.
@@ -112,17 +121,18 @@ class Definitions(WObject):
     @type children: [L{WObject},...]
     @ivar imports: The list of L{Import} children.
     @type imports: [L{Import},...]
-    @ivar messages: The dictionary of L{Message} children key'd by I{qname}
+    @ivar messages: The dictionary of L{Message} children keyed by I{qname}.
     @type messages: [L{Message},...]
-    @ivar port_types: The dictionary of L{PortType} children key'd by I{qname}
+    @ivar port_types: The dictionary of L{PortType} children keyed by I{qname}.
     @type port_types: [L{PortType},...]
-    @ivar bindings: The dictionary of L{Binding} children key'd by I{qname}
+    @ivar bindings: The dictionary of L{Binding} children keyed by I{qname}.
     @type bindings: [L{Binding},...]
     @ivar service: The service object.
     @type service: L{Service}
+
     """
 
-    Tag = 'definitions'
+    Tag = "definitions"
 
     def __init__(self, url, options):
         """
@@ -130,8 +140,9 @@ class Definitions(WObject):
         @type url: str
         @param options: An options dictionary.
         @type options: L{options.Options}
+
         """
-        log.debug('reading WSDL at: %s ...', url)
+        log.debug("reading WSDL at: %s ...", url)
         reader = DocumentReader(options)
         d = reader.open(url)
         root = d.root()
@@ -151,9 +162,9 @@ class Definitions(WObject):
         self.add_children(self.root)
         self.children.sort()
         pmd = self.__metadata__.__print__
-        pmd.excludes.append('children')
-        pmd.excludes.append('wsdl')
-        pmd.wrappers['schema'] = repr
+        pmd.excludes.append("children")
+        pmd.excludes.append("wsdl")
+        pmd.wrappers["schema"] = repr
         self.open_imports()
         self.resolve()
         self.build_schema()
@@ -163,16 +174,16 @@ class Definitions(WObject):
         log.debug("WSDL at '%s' loaded:\n%s", url, self)
 
     def mktns(self, root):
-        """ Get/create the target namespace """
-        tns = root.get('targetNamespace')
+        """Get/create the target namespace."""
+        tns = root.get("targetNamespace")
         prefix = root.findPrefix(tns)
         if prefix is None:
-            log.debug('warning: tns (%s), not mapped to prefix', tns)
-            prefix = 'tns'
+            log.debug("warning: tns (%s), not mapped to prefix", tns)
+            prefix = "tns"
         return (prefix, tns)
 
     def add_children(self, root):
-        """ Add child objects using the factory """
+        """Add child objects using the factory."""
         for c in root.getChildren(ns=wsdlns):
             child = Factory.create(c, self)
             if child is None: continue
@@ -197,24 +208,24 @@ class Definitions(WObject):
                 continue
 
     def open_imports(self):
-        """ Import the I{imported} WSDLs. """
+        """Import the I{imported} WSDLs."""
         for imp in self.imports:
             imp.load(self)
 
     def resolve(self):
-        """ Tell all children to resolve themselves """
+        """Tell all children to resolve themselves."""
         for c in self.children:
             c.resolve(self)
 
     def build_schema(self):
-        """ Process L{Types} objects and create the schema collection """
+        """Process L{Types} objects and create the schema collection."""
         container = SchemaCollection(self)
         for t in [t for t in self.types if t.local()]:
             for root in t.contents():
                 schema = Schema(root, self.url, self.options, container)
                 container.add(schema)
         if not len(container):
-            root = Element.buildPath(self.root, 'types/schema')
+            root = Element.buildPath(self.root, "types/schema")
             schema = Schema(root, self.url, self.options, container)
             container.add(schema)
         self.schema = container.load(self.options)
@@ -223,32 +234,31 @@ class Definitions(WObject):
         return self.schema
 
     def add_methods(self, service):
-        """ Build method view for service """
+        """Build method view for service."""
         bindings = {
-            'document/literal' : Document(self),
-            'rpc/literal' : RPC(self),
-            'rpc/encoded' : Encoded(self)
-        }
+            "document/literal": Document(self),
+            "rpc/literal": RPC(self),
+            "rpc/encoded": Encoded(self)}
         for p in service.ports:
             binding = p.binding
             ptype = p.binding.type
             operations = p.binding.type.operations.values()
             for name in [op.name for op in operations]:
-                m = Facade('Method')
+                m = Facade("Method")
                 m.name = name
                 m.location = p.location
-                m.binding = Facade('binding')
+                m.binding = Facade("binding")
                 op = binding.operation(name)
                 m.soap = op.soap
-                key = '/'.join((op.soap.style, op.soap.input.body.use))
+                key = "/".join((op.soap.style, op.soap.input.body.use))
                 m.binding.input = bindings.get(key)
-                key = '/'.join((op.soap.style, op.soap.output.body.use))
+                key = "/".join((op.soap.style, op.soap.output.body.use))
                 m.binding.output = bindings.get(key)
                 op = ptype.operation(name)
                 p.methods[name] = m
 
     def set_wrapped(self):
-        """ set (wrapped|bare) flag on messages """
+        """Set (wrapped|bare) flag on messages."""
         for b in self.bindings.values():
             for op in b.operations.values():
                 for body in (op.soap.input.body, op.soap.output.body):
@@ -270,7 +280,7 @@ class Definitions(WObject):
                         body.wrapped = True
 
     def __getstate__(self):
-        nopickle = ('options',)
+        nopickle = ("options",)
         state = self.__dict__.copy()
         for k in nopickle:
             if k in state:
@@ -278,18 +288,20 @@ class Definitions(WObject):
         return state
 
     def __repr__(self):
-        return 'Definitions (id=%s)' % self.id
+        return "Definitions (id=%s)" % (self.id,)
 
 
 class Import(WObject):
     """
     Represents the <wsdl:import/>.
+
     @ivar location: The value of the I{location} attribute.
     @type location: str
     @ivar ns: The value of the I{namespace} attribute.
     @type ns: str
     @ivar imported: The imported object.
     @type imported: L{Definitions}
+
     """
 
     def __init__(self, root, definitions):
@@ -298,19 +310,20 @@ class Import(WObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         WObject.__init__(self, root)
-        self.location = root.get('location')
-        self.ns = root.get('namespace')
+        self.location = root.get("location")
+        self.ns = root.get("namespace")
         self.imported = None
         pmd = self.__metadata__.__print__
-        pmd.wrappers['imported'] = repr
+        pmd.wrappers["imported"] = repr
 
     def load(self, definitions):
-        """ Load the object by opening the URL """
+        """Load the object by opening the URL."""
         url = self.location
-        log.debug('importing (%s)', url)
-        if '://' not in url:
+        log.debug("importing (%s)", url)
+        if "://" not in url:
             url = urljoin(definitions.url, url)
         options = definitions.options
         d = Definitions(url, options)
@@ -320,37 +333,35 @@ class Import(WObject):
         if d.root.match(Schema.Tag, Namespace.xsdns):
             self.import_schema(definitions, d)
             return
-        raise Exception('document at "%s" is unknown' % url)
+        raise Exception("document at '%s' is unknown" % url)
 
     def import_definitions(self, definitions, d):
-        """ import/merge WSDL definitions """
+        """Import/merge WSDL definitions."""
         definitions.types += d.types
         definitions.messages.update(d.messages)
         definitions.port_types.update(d.port_types)
         definitions.bindings.update(d.bindings)
         self.imported = d
-        log.debug('imported (WSDL):\n%s', d)
+        log.debug("imported (WSDL):\n%s", d)
 
     def import_schema(self, definitions, d):
-        """ import schema as <types/> content """
+        """Import schema as <types/> content."""
         if not len(definitions.types):
-            root = Element('types', ns=wsdlns)
+            root = Element("types", ns=wsdlns)
             definitions.root.insert(root)
             types = Types(root, definitions)
             definitions.types.append(types)
         else:
             types = definitions.types[-1]
         types.root.append(d.root)
-        log.debug('imported (XSD):\n%s', d.root)
+        log.debug("imported (XSD):\n%s", d.root)
 
     def __gt__(self, other):
         return False
 
 
 class Types(WObject):
-    """
-    Represents <types><schema/></types>.
-    """
+    """Represents <types><schema/></types>."""
 
     def __init__(self, root, definitions):
         """
@@ -358,21 +369,22 @@ class Types(WObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         WObject.__init__(self, root)
         self.definitions = definitions
 
     def contents(self):
-        return self.root.getChildren('schema', Namespace.xsdns)
+        return self.root.getChildren("schema", Namespace.xsdns)
 
     def schema(self):
         return self.definitions.schema
 
     def local(self):
-        return ( self.definitions.schema is None )
+        return self.definitions.schema is None
 
     def imported(self):
-        return ( not self.local() )
+        return not self.local()
 
     def __gt__(self, other):
         return isinstance(other, Import)
@@ -381,12 +393,14 @@ class Types(WObject):
 class Part(NamedObject):
     """
     Represents <message><part/></message>.
-    @ivar element: The value of the {element} attribute.
-        Stored as a I{qref} as converted by L{suds.xsd.qualify}.
+
+    @ivar element: The value of the {element} attribute. Stored as a I{qref} as
+        converted by L{suds.xsd.qualify}.
     @type element: str
-    @ivar type: The value of the {type} attribute.
-        Stored as a I{qref} as converted by L{suds.xsd.qualify}.
+    @ivar type: The value of the {type} attribute. Stored as a I{qref} as
+        converted by L{suds.xsd.qualify}.
     @type type: str
+
     """
 
     def __init__(self, root, definitions):
@@ -395,29 +409,30 @@ class Part(NamedObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         NamedObject.__init__(self, root, definitions)
         pmd = Metadata()
         pmd.wrappers = dict(element=repr, type=repr)
         self.__metadata__.__print__ = pmd
         tns = definitions.tns
-        self.element = self.__getref('element', tns)
-        self.type = self.__getref('type', tns)
+        self.element = self.__getref("element", tns)
+        self.type = self.__getref("type", tns)
 
     def __getref(self, a, tns):
-        """ Get the qualified value of attribute named 'a'."""
+        """Get the qualified value of attribute named 'a'."""
         s = self.root.get(a)
-        if s is None:
-            return s
-        else:
+        if s is not None:
             return qualify(s, self.root, tns)
 
 
 class Message(NamedObject):
     """
     Represents <message/>.
+
     @ivar parts: A list of message parts.
     @type parts: [I{Part},...]
+
     """
 
     def __init__(self, root, definitions):
@@ -426,10 +441,11 @@ class Message(NamedObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         NamedObject.__init__(self, root, definitions)
         self.parts = []
-        for p in root.getChildren('part'):
+        for p in root.getChildren("part"):
             part = Part(p, definitions)
             self.parts.append(part)
 
@@ -440,8 +456,10 @@ class Message(NamedObject):
 class PortType(NamedObject):
     """
     Represents <portType/>.
+
     @ivar operations: A list of contained operations.
     @type operations: list
+
     """
 
     def __init__(self, root, definitions):
@@ -450,28 +468,29 @@ class PortType(NamedObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         NamedObject.__init__(self, root, definitions)
         self.operations = {}
-        for c in root.getChildren('operation'):
-            op = Facade('Operation')
-            op.name = c.get('name')
+        for c in root.getChildren("operation"):
+            op = Facade("Operation")
+            op.name = c.get("name")
             op.tns = definitions.tns
-            input = c.getChild('input')
+            input = c.getChild("input")
             if input is None:
                 op.input = None
             else:
-                op.input = input.get('message')
-            output = c.getChild('output')
+                op.input = input.get("message")
+            output = c.getChild("output")
             if output is None:
                 op.output = None
             else:
-                op.output = output.get('message')
+                op.output = output.get("message")
             faults = []
-            for fault in c.getChildren('fault'):
-                f = Facade('Fault')
-                f.name = fault.get('name')
-                f.message = fault.get('message')
+            for fault in c.getChildren("fault"):
+                f = Facade("Fault")
+                f.name = fault.get("name")
+                f.message = fault.get("message")
                 faults.append(f)
             op.faults = faults
             self.operations[op.name] = op
@@ -479,43 +498,45 @@ class PortType(NamedObject):
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         for op in self.operations.values():
             if op.input is None:
-                op.input = Message(Element('no-input'), definitions)
+                op.input = Message(Element("no-input"), definitions)
             else:
                 qref = qualify(op.input, self.root, definitions.tns)
                 msg = definitions.messages.get(qref)
                 if msg is None:
-                    raise Exception("msg '%s', not-found" % op.input)
-                else:
-                    op.input = msg
+                    raise Exception("msg '%s', not-found" % (op.input,))
+                op.input = msg
             if op.output is None:
-                op.output = Message(Element('no-output'), definitions)
+                op.output = Message(Element("no-output"), definitions)
             else:
                 qref = qualify(op.output, self.root, definitions.tns)
                 msg = definitions.messages.get(qref)
                 if msg is None:
-                    raise Exception("msg '%s', not-found" % op.output)
-                else:
-                    op.output = msg
+                    raise Exception("msg '%s', not-found" % (op.output,))
+                op.output = msg
             for f in op.faults:
                 qref = qualify(f.message, self.root, definitions.tns)
                 msg = definitions.messages.get(qref)
                 if msg is None:
-                    raise Exception, "msg '%s', not-found" % f.message
+                    raise Exception("msg '%s', not-found" % (f.message,))
                 f.message = msg
 
     def operation(self, name):
         """
         Shortcut used to get a contained operation by name.
+
         @param name: An operation name.
         @type name: str
         @return: The named operation.
         @rtype: Operation
         @raise L{MethodNotFound}: When not found.
+
         """
         try:
             return self.operations[name]
@@ -528,9 +549,11 @@ class PortType(NamedObject):
 
 class Binding(NamedObject):
     """
-    Represents <binding/>
+    Represents <binding/>.
+
     @ivar operations: A list of contained operations.
     @type operations: list
+
     """
 
     def __init__(self, root, definitions):
@@ -539,119 +562,121 @@ class Binding(NamedObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         NamedObject.__init__(self, root, definitions)
         self.operations = {}
-        self.type = root.get('type')
+        self.type = root.get("type")
         sr = self.soaproot()
         if sr is None:
             self.soap = None
-            log.debug('binding: "%s" not a SOAP binding', self.name)
+            log.debug("binding: '%s' not a SOAP binding", self.name)
             return
-        soap = Facade('soap')
+        soap = Facade("soap")
         self.soap = soap
-        self.soap.style = sr.get('style', default='document')
+        self.soap.style = sr.get("style", default="document")
         self.add_operations(self.root, definitions)
 
     def soaproot(self):
-        """ get the soap:binding """
+        """Get the soap:binding."""
         for ns in (soapns, soap12ns):
-            sr = self.root.getChild('binding', ns=ns)
+            sr = self.root.getChild("binding", ns=ns)
             if sr is not None:
                 return sr
-        return None
 
     def add_operations(self, root, definitions):
-        """ Add <operation/> children """
-        dsop = Element('operation', ns=soapns)
-        for c in root.getChildren('operation'):
-            op = Facade('Operation')
-            op.name = c.get('name')
-            sop = c.getChild('operation', default=dsop)
-            soap = Facade('soap')
-            soap.action = '"%s"' % sop.get('soapAction', default='')
-            soap.style = sop.get('style', default=self.soap.style)
-            soap.input = Facade('Input')
-            soap.input.body = Facade('Body')
+        """Add <operation/> children."""
+        dsop = Element("operation", ns=soapns)
+        for c in root.getChildren("operation"):
+            op = Facade("Operation")
+            op.name = c.get("name")
+            sop = c.getChild("operation", default=dsop)
+            soap = Facade("soap")
+            soap.action = '"%s"' % (sop.get("soapAction", default=""),)
+            soap.style = sop.get("style", default=self.soap.style)
+            soap.input = Facade("Input")
+            soap.input.body = Facade("Body")
             soap.input.headers = []
-            soap.output = Facade('Output')
-            soap.output.body = Facade('Body')
+            soap.output = Facade("Output")
+            soap.output.body = Facade("Body")
             soap.output.headers = []
             op.soap = soap
-            input = c.getChild('input')
+            input = c.getChild("input")
             if input is None:
-                input = Element('input', ns=wsdlns)
-            body = input.getChild('body')
+                input = Element("input", ns=wsdlns)
+            body = input.getChild("body")
             self.body(definitions, soap.input.body, body)
-            for header in input.getChildren('header'):
+            for header in input.getChildren("header"):
                 self.header(definitions, soap.input, header)
-            output = c.getChild('output')
+            output = c.getChild("output")
             if output is None:
-                output = Element('output', ns=wsdlns)
-            body = output.getChild('body')
+                output = Element("output", ns=wsdlns)
+            body = output.getChild("body")
             self.body(definitions, soap.output.body, body)
-            for header in output.getChildren('header'):
+            for header in output.getChildren("header"):
                 self.header(definitions, soap.output, header)
             faults = []
-            for fault in c.getChildren('fault'):
-                sf = fault.getChild('fault')
+            for fault in c.getChildren("fault"):
+                sf = fault.getChild("fault")
                 if sf is None:
                     continue
-                fn = fault.get('name')
-                f = Facade('Fault')
-                f.name = sf.get('name', default=fn)
-                f.use = sf.get('use', default='literal')
+                fn = fault.get("name")
+                f = Facade("Fault")
+                f.name = sf.get("name", default=fn)
+                f.use = sf.get("use", default="literal")
                 faults.append(f)
             soap.faults = faults
             self.operations[op.name] = op
 
     def body(self, definitions, body, root):
-        """ add the input/output body properties """
+        """Add the input/output body properties."""
         if root is None:
-            body.use = 'literal'
+            body.use = "literal"
             body.namespace = definitions.tns
             body.parts = ()
             return
-        parts = root.get('parts')
+        parts = root.get("parts")
         if parts is None:
             body.parts = ()
         else:
-            body.parts = re.split('[\s,]', parts)
-        body.use = root.get('use', default='literal')
-        ns = root.get('namespace')
+            body.parts = re.split("[\s,]", parts)
+        body.use = root.get("use", default="literal")
+        ns = root.get("namespace")
         if ns is None:
             body.namespace = definitions.tns
         else:
-            prefix = root.findPrefix(ns, 'b0')
+            prefix = root.findPrefix(ns, "b0")
             body.namespace = (prefix, ns)
 
     def header(self, definitions, parent, root):
-        """ add the input/output header properties """
+        """Add the input/output header properties."""
         if root is None:
             return
-        header = Facade('Header')
+        header = Facade("Header")
         parent.headers.append(header)
-        header.use = root.get('use', default='literal')
-        ns = root.get('namespace')
+        header.use = root.get("use", default="literal")
+        ns = root.get("namespace")
         if ns is None:
             header.namespace = definitions.tns
         else:
-            prefix = root.findPrefix(ns, 'h0')
+            prefix = root.findPrefix(ns, "h0")
             header.namespace = (prefix, ns)
-        msg = root.get('message')
+        msg = root.get("message")
         if msg is not None:
             header.message = msg
-        part = root.get('part')
+        part = root.get("part")
         if part is not None:
             header.part = part
 
     def resolve(self, definitions):
         """
-        Resolve named references to other WSDL objects.  This includes
-        cross-linking information (from) the portType (to) the I{SOAP}
-        protocol information on the binding for each operation.
+        Resolve named references to other WSDL objects. This includes
+        cross-linking information (from) the portType (to) the I{SOAP} protocol
+        information on the binding for each operation.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         self.resolveport(definitions)
         for op in self.operations.values():
@@ -662,29 +687,32 @@ class Binding(NamedObject):
     def resolveport(self, definitions):
         """
         Resolve port_type reference.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         ref = qualify(self.type, self.root, definitions.tns)
         port_type = definitions.port_types.get(ref)
         if port_type is None:
-            raise Exception("portType '%s', not-found" % self.type)
-        else:
-            self.type = port_type
+            raise Exception("portType '%s', not-found" % (self.type,))
+        self.type = port_type
 
     def resolvesoapbody(self, definitions, op):
         """
-        Resolve SOAP body I{message} parts by
-        cross-referencing with operation defined in port type.
+        Resolve SOAP body I{message} parts by cross-referencing with operation
+        defined in port type.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @param op: An I{operation} object.
         @type op: I{operation}
+
         """
         ptop = self.type.operation(op.name)
         if ptop is None:
-            raise Exception, \
-                "operation '%s' not defined in portType" % op.name
+            raise Exception("operation '%s' not defined in portType" % (
+                op.name,))
         soap = op.soap
         parts = soap.input.body.parts
         if len(parts):
@@ -708,10 +736,12 @@ class Binding(NamedObject):
     def resolveheaders(self, definitions, op):
         """
         Resolve SOAP header I{message} references.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @param op: An I{operation} object.
         @type op: I{operation}
+
         """
         soap = op.soap
         headers = soap.input.headers + soap.output.headers
@@ -720,67 +750,73 @@ class Binding(NamedObject):
             ref = qualify(mn, self.root, definitions.tns)
             message = definitions.messages.get(ref)
             if message is None:
-                raise Exception, "message'%s', not-found" % mn
+                raise Exception("message '%s', not-found" % (mn,))
             pn = header.part
             for p in message.parts:
                 if p.name == pn:
                     header.part = p
                     break
             if pn == header.part:
-                raise Exception, \
-                    "message '%s' has not part named '%s'" % (ref, pn)
+                raise Exception("message '%s' has not part named '%s'" % (
+                    ref, pn))
 
     def resolvefaults(self, definitions, op):
         """
-        Resolve SOAP fault I{message} references by
-        cross-referencing with operations defined in the port type.
+        Resolve SOAP fault I{message} references by cross-referencing with
+        operations defined in the port type.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @param op: An I{operation} object.
         @type op: I{operation}
+
         """
         ptop = self.type.operation(op.name)
         if ptop is None:
-            raise Exception, \
-                "operation '%s' not defined in portType" % op.name
+            raise Exception("operation '%s' not defined in portType" % (
+                op.name,))
         soap = op.soap
         for fault in soap.faults:
             for f in ptop.faults:
                 if f.name == fault.name:
                     fault.parts = f.message.parts
                     continue
-            if hasattr(fault, 'parts'):
+            if hasattr(fault, "parts"):
                 continue
-            raise Exception, \
-                "fault '%s' not defined in portType '%s'" % (fault.name, self.type.name)
+            raise Exception("fault '%s' not defined in portType '%s'" % (
+                fault.name, self.type.name))
 
     def operation(self, name):
         """
         Shortcut used to get a contained operation by name.
+
         @param name: An operation name.
         @type name: str
         @return: The named operation.
         @rtype: Operation
         @raise L{MethodNotFound}: When not found.
+
         """
         try:
             return self.operations[name]
-        except:
+        except Exception:
             raise MethodNotFound(name)
 
     def __gt__(self, other):
-        return ( not isinstance(other, Service) )
+        return not isinstance(other, Service)
 
 
 class Port(NamedObject):
     """
     Represents a service port.
+
     @ivar service: A service.
     @type service: L{Service}
     @ivar binding: A binding name.
     @type binding: str
     @ivar location: The service location (URL).
     @type location: str
+
     """
 
     def __init__(self, root, definitions, service):
@@ -791,21 +827,24 @@ class Port(NamedObject):
         @type definitions: L{Definitions}
         @param service: A service object.
         @type service: L{Service}
+
         """
         NamedObject.__init__(self, root, definitions)
         self.__service = service
-        self.binding = root.get('binding')
-        address = root.getChild('address')
-        self.location = address is not None and address.get('location')
+        self.binding = root.get("binding")
+        address = root.getChild("address")
+        self.location = address is not None and address.get("location")
         self.methods = {}
 
     def method(self, name):
         """
         Get a method defined in this portType by name.
+
         @param name: A method name.
         @type name: str
         @return: The requested method object.
         @rtype: I{Method}
+
         """
         return self.methods.get(name)
 
@@ -813,10 +852,12 @@ class Port(NamedObject):
 class Service(NamedObject):
     """
     Represents <service/>.
+
     @ivar port: The contained ports.
     @type port: [Port,..]
     @ivar methods: The contained methods for all ports.
     @type methods: [Method,..]
+
     """
 
     def __init__(self, root, definitions):
@@ -825,33 +866,37 @@ class Service(NamedObject):
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         NamedObject.__init__(self, root, definitions)
         self.ports = []
-        for p in root.getChildren('port'):
+        for p in root.getChildren("port"):
             port = Port(p, definitions, self)
             self.ports.append(port)
 
     def port(self, name):
         """
         Locate a port by name.
+
         @param name: A port name.
         @type name: str
         @return: The port object.
         @rtype: L{Port}
+
         """
         for p in self.ports:
             if p.name == name:
                 return p
-        return None
 
     def setlocation(self, url, names=None):
         """
         Override the invocation location (URL) for service method.
+
         @param url: A URL location.
         @type url: A URL.
-        @param names:  A list of method names.  None=ALL
+        @param names:  A list of method names. None=ALL
         @type names: [str,..]
+
         """
         for p in self.ports:
             for m in p.methods.values():
@@ -860,19 +905,22 @@ class Service(NamedObject):
 
     def resolve(self, definitions):
         """
-        Resolve named references to other WSDL objects.
-        Ports without SOAP bindings are discarded.
+        Resolve named references to other WSDL objects. Ports without SOAP
+        bindings are discarded.
+
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
+
         """
         filtered = []
         for p in self.ports:
             ref = qualify(p.binding, self.root, definitions.tns)
             binding = definitions.bindings.get(ref)
             if binding is None:
-                raise Exception("binding '%s', not-found" % p.binding)
+                raise Exception("binding '%s', not-found" % (p.binding,))
             if binding.soap is None:
-                log.debug('binding "%s" - not a SOAP binding, discarded', binding.name)
+                log.debug("binding '%s' - not a SOAP binding, discarded",
+                    binding.name)
                 continue
             p.binding = binding
             filtered.append(p)
@@ -885,33 +933,33 @@ class Service(NamedObject):
 class Factory:
     """
     Simple WSDL object factory.
-    @cvar tags: Dictionary of tag->constructor mappings.
+
+    @cvar tags: Dictionary of tag-->constructor mappings.
     @type tags: dict
+
     """
 
-    tags =\
-    {
-        'import' : Import,
-        'types' : Types,
-        'message' : Message,
-        'portType' : PortType,
-        'binding' : Binding,
-        'service' : Service,
-    }
+    tags = {
+        "import" : Import,
+        "types" : Types,
+        "message" : Message,
+        "portType" : PortType,
+        "binding" : Binding,
+        "service" : Service}
 
     @classmethod
     def create(cls, root, definitions):
         """
         Create an object based on the root tag name.
+
         @param root: An XML root element.
         @type root: L{Element}
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @return: The created object.
         @rtype: L{WObject}
+
         """
         fn = cls.tags.get(root.name)
         if fn is not None:
             return fn(root, definitions)
-        else:
-            return None
diff --git a/suds/wsse.py b/suds/wsse.py
index c2f7f52..2a091d0 100644
--- a/suds/wsse.py
+++ b/suds/wsse.py
@@ -94,7 +94,7 @@ class Token(Object):
 
     @classmethod
     def sysdate(cls):
-        utc = DateTime(self.utc())
+        utc = DateTime(cls.utc())
         return str(utc)
 
     def __init__(self):
diff --git a/suds/xsd/deplist.py b/suds/xsd/deplist.py
deleted file mode 100644
index b813f80..0000000
--- a/suds/xsd/deplist.py
+++ /dev/null
@@ -1,140 +0,0 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-"""
-The I{depsolve} module defines a class for performing dependency solving.
-"""
-
-from suds import *
-
-from logging import getLogger
-log = getLogger(__name__)
-
-
-class DepList:
-    """
-    Dependency solving list.
-    Items are tuples: (object, (deps,))
-    @ivar raw: The raw (unsorted) items.
-    @type raw: list
-    @ivar index: The index of (unsorted) items.
-    @type index: list
-    @ivar stack: The sorting stack.
-    @type stack: list
-    @ivar pushed: The I{pushed} set tracks items that have been
-        processed.
-    @type pushed: set
-    @ivar sorted: The sorted list of items.
-    @type sorted: list
-    """
-
-    def __init__(self):
-        """ """
-        self.unsorted = []
-        self.index = {}
-        self.stack = []
-        self.pushed = set()
-        self.sorted = None
-
-    def add(self, *items):
-        """
-        Add items to be sorted.
-        @param items: One or more items to be added.
-        @type items: I{item}
-        @return: self
-        @rtype: L{DepList}
-        """
-        for item in items:
-            self.unsorted.append(item)
-            key = item[0]
-            self.index[key] = item
-        return self
-
-    def sort(self):
-        """
-        Sort the list based on dependencies.
-        @return: The sorted items.
-        @rtype: list
-        """
-        self.sorted = list()
-        self.pushed = set()
-        for item in self.unsorted:
-            popped = []
-            self.push(item)
-            while len(self.stack):
-                try:
-                    top = self.top()
-                    ref = top[1].next()
-                    refd = self.index.get(ref)
-                    if refd is None:
-                        log.debug('"%s" not found, skipped', Repr(ref))
-                        continue
-                    self.push(refd)
-                except StopIteration:
-                    popped.append(self.pop())
-                    continue
-            for p in popped:
-                self.sorted.append(p)
-        self.unsorted = self.sorted
-        return self.sorted
-
-    def top(self):
-        """
-        Get the item at the top of the stack.
-        @return: The top item.
-        @rtype: (item, iter)
-        """
-        return self.stack[-1]
-
-    def push(self, item):
-        """
-        Push and item onto the sorting stack.
-        @param item: An item to push.
-        @type item: I{item}
-        @return: The number of items pushed.
-        @rtype: int
-        """
-        if item in self.pushed:
-            return
-        frame = (item, iter(item[1]))
-        self.stack.append(frame)
-        self.pushed.add(item)
-
-    def pop(self):
-        """
-        Pop the top item off the stack and append
-        it to the sorted list.
-        @return: The popped item.
-        @rtype: I{item}
-        """
-        try:
-            frame = self.stack.pop()
-            return frame[0]
-        except:
-            pass
-
-
-if __name__ == '__main__':
-    a = ('a', ('x',))
-    b = ('b', ('a',))
-    c = ('c', ('a','b'))
-    d = ('d', ('c',))
-    e = ('e', ('d','a'))
-    f = ('f', ('e','c','d','a'))
-    x = ('x', ())
-    L = DepList()
-    L.add(c, e, d, b, f, a, x)
-    print [x[0] for x in L.sort()]
diff --git a/suds/xsd/depsort.py b/suds/xsd/depsort.py
new file mode 100644
index 0000000..e328da1
--- /dev/null
+++ b/suds/xsd/depsort.py
@@ -0,0 +1,71 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel at redhat.com )
+
+"""
+Dependency/topological sort implementation.
+
+"""
+
+from suds import *
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+def dependency_sort(dependency_tree):
+    """
+    Sorts items 'dependencies first' in a given dependency tree.
+
+    A dependency tree is a dictionary mapping an object to a collection its
+    dependency objects.
+
+    Result is property sorted list of items, where each item is a 2-tuple
+    containing an object and its dependency list, as given in the input
+    dependency tree.
+
+    If B is directly or indirectly dependent on A and they are not both a part
+    of the same dependency cycle (i.e. then A is neither directly nor
+    indirectly dependent on B) then A needs to come before B.
+
+    If A and B are a part of the same dependency cycle, i.e. if they are both
+    directly or indirectly dependent on each other, then it does not matter
+    which comes first.
+
+    Any entries found listed as dependencies, but that do not have their own
+    dependencies listed as well, are logged & ignored.
+
+    @return: The sorted items.
+    @rtype: list
+
+    """
+    sorted = []
+    processed = set()
+    for key, deps in dependency_tree.iteritems():
+        _sort_r(sorted, processed, key, deps, dependency_tree)
+    return sorted
+
+
+def _sort_r(sorted, processed, key, deps, dependency_tree):
+    """Recursive topological sort implementation."""
+    if key in processed:
+        return
+    processed.add(key)
+    for dep_key in deps:
+        dep_deps = dependency_tree.get(dep_key)
+        if dep_deps is None:
+            log.debug('"%s" not found, skipped', Repr(dep_key))
+            continue
+        _sort_r(sorted, processed, dep_key, dep_deps, dependency_tree)
+    sorted.append((key, deps))
diff --git a/suds/xsd/schema.py b/suds/xsd/schema.py
index 00e217f..2066a2f 100644
--- a/suds/xsd/schema.py
+++ b/suds/xsd/schema.py
@@ -1,37 +1,36 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{schema} module provides an intelligent representation of
-an XSD schema.  The I{raw} model is the XML tree and the I{model}
-is the denormalized, objectified and intelligent view of the schema.
-Most of the I{value-add} provided by the model is centered around
-transparent referenced type resolution and targeted denormalization.
-"""
+The I{schema} module provides an intelligent representation of an XSD schema.
+The I{raw} model is the XML tree and the I{model} is a denormalized,
+objectified and intelligent view of the schema. Most of the I{value-add}
+provided by the model is centered around transparent referenced type resolution
+and targeted denormalization.
 
+"""
 
 from suds import *
 from suds.xsd import *
+from suds.xsd.depsort import dependency_sort
 from suds.xsd.sxbuiltin import *
+from suds.xsd.sxbase import SchemaObject
 from suds.xsd.sxbasic import Factory as BasicFactory
 from suds.xsd.sxbuiltin import Factory as BuiltinFactory
-from suds.xsd.sxbase import SchemaObject
-from suds.xsd.deplist import DepList
-from suds.sax.element import Element
 from suds.sax import splitPrefix, Namespace
+from suds.sax.element import Element
 
 from logging import getLogger
 log = getLogger(__name__)
@@ -39,20 +38,24 @@ log = getLogger(__name__)
 
 class SchemaCollection(UnicodeMixin):
     """
-    A collection of schema objects.  This class is needed because WSDLs
-    may contain more then one <schema/> node.
-    @ivar wsdl: A wsdl object.
+    A collection of schema objects.
+
+    This class is needed because a WSDL may contain more then one <schema/>
+    node.
+
+    @ivar wsdl: A WSDL object.
     @type wsdl: L{suds.wsdl.Definitions}
     @ivar children: A list contained schemas.
     @type children: [L{Schema},...]
     @ivar namespaces: A dictionary of contained schemas by namespace.
-    @type namespaces: {str:L{Schema}}
+    @type namespaces: {str: L{Schema}}
     """
 
     def __init__(self, wsdl):
         """
-        @param wsdl: A wsdl object.
+        @param wsdl: A WSDL object.
         @type wsdl: L{suds.wsdl.Definitions}
+
         """
         self.wsdl = wsdl
         self.children = []
@@ -60,10 +63,12 @@ class SchemaCollection(UnicodeMixin):
 
     def add(self, schema):
         """
-        Add a schema node to the collection.  Schema(s) within the same target
+        Add a schema node to the collection. Schema(s) within the same target
         namespace are consolidated.
+
         @param schema: A schema object.
         @type schema: (L{Schema})
+
         """
         key = schema.tns[1]
         existing = self.namespaces.get(key)
@@ -76,13 +81,15 @@ class SchemaCollection(UnicodeMixin):
 
     def load(self, options):
         """
-        Load the schema objects for the root nodes.
-            - de-references schemas
+        Load schema objects for the root nodes.
+            - de-reference schemas
             - merge schemas
+
         @param options: An options dictionary.
         @type options: L{options.Options}
         @return: The merged schema.
         @rtype: L{Schema}
+
         """
         if options.autoblend:
             self.autoblend()
@@ -92,71 +99,76 @@ class SchemaCollection(UnicodeMixin):
             child.open_imports(options)
         for child in self.children:
             child.dereference()
-        log.debug('loaded:\n%s', self)
+        log.debug("loaded:\n%s", self)
         merged = self.merge()
-        log.debug('MERGED:\n%s', merged)
+        log.debug("MERGED:\n%s", merged)
         return merged
 
     def autoblend(self):
         """
-        Ensure that all schemas within the collection
-        import each other which has a blending effect.
+        Ensure that all schemas within the collection import each other which
+        has a blending effect.
+
         @return: self
         @rtype: L{SchemaCollection}
+
         """
         namespaces = self.namespaces.keys()
         for s in self.children:
             for ns in namespaces:
-                tns = s.root.get('targetNamespace')
+                tns = s.root.get("targetNamespace")
                 if tns == ns:
                     continue
-                for imp in s.root.getChildren('import'):
-                    if imp.get('namespace') == ns:
+                for imp in s.root.getChildren("import"):
+                    if imp.get("namespace") == ns:
                         continue
-                imp = Element('import', ns=Namespace.xsdns)
-                imp.set('namespace', ns)
+                imp = Element("import", ns=Namespace.xsdns)
+                imp.set("namespace", ns)
                 s.root.append(imp)
         return self
 
     def locate(self, ns):
         """
-        Find a schema by namespace.  Only the URI portion of
-        the namespace is compared to each schema's I{targetNamespace}
+        Find a schema by namespace. Only the URI portion of the namespace is
+        compared to each schema's I{targetNamespace}.
+
         @param ns: A namespace.
-        @type ns: (prefix,URI)
+        @type ns: (prefix, URI)
         @return: The schema matching the namespace, else None.
         @rtype: L{Schema}
+
         """
         return self.namespaces.get(ns[1])
 
     def merge(self):
         """
-        Merge the contained schemas into one.
+        Merge contained schemas into one.
+
         @return: The merged schema.
         @rtype: L{Schema}
+
         """
-        if len(self):
+        if self.children:
             schema = self.children[0]
             for s in self.children[1:]:
                 schema.merge(s)
             return schema
-        else:
-            return None
 
     def __len__(self):
         return len(self.children)
 
     def __unicode__(self):
-        result = ['\nschema collection']
+        result = ["\nschema collection"]
         for s in self.children:
             result.append(s.str(1))
-        return '\n'.join(result)
+        return "\n".join(result)
 
 
 class Schema(UnicodeMixin):
     """
-    The schema is an objectification of a <schema/> (xsd) definition.
-    It provides inspection, lookup and type resolution.
+    The schema is an objectification of a <schema/> (XSD) definition. It
+    provides inspection, lookup and type resolution.
+
     @ivar root: The root node.
     @type root: L{sax.element.Element}
     @ivar baseurl: The I{base} URL for this schema.
@@ -179,23 +191,24 @@ class Schema(UnicodeMixin):
     @type groups: [L{SchemaObject},...]
     @ivar agrps: A list of attribute group objects.
     @type agrps: [L{SchemaObject},...]
-    @ivar form_qualified: The flag indicating:
-        (@elementFormDefault).
+    @ivar form_qualified: The flag indicating: (@elementFormDefault).
     @type form_qualified: bool
+
     """
 
-    Tag = 'schema'
+    Tag = "schema"
 
     def __init__(self, root, baseurl, options, container=None):
         """
-        @param root: The xml root.
+        @param root: The XML root.
         @type root: L{sax.element.Element}
-        @param baseurl: The base url used for importing.
+        @param baseurl: The base URL used for importing.
         @type baseurl: basestring
         @param options: An options dictionary.
         @type options: L{options.Options}
         @param container: An optional container.
         @type container: L{SchemaCollection}
+
         """
         self.root = root
         self.id = objid(self)
@@ -212,36 +225,36 @@ class Schema(UnicodeMixin):
         self.agrps = {}
         if options.doctor is not None:
             options.doctor.examine(root)
-        form = self.root.get('elementFormDefault')
-        if form is None:
-            self.form_qualified = False
-        else:
-            self.form_qualified = ( form == 'qualified' )
+        form = self.root.get("elementFormDefault")
+        self.form_qualified = form == "qualified"
         if container is None:
             self.build()
             self.open_imports(options)
-            log.debug('built:\n%s', self)
+            log.debug("built:\n%s", self)
             self.dereference()
-            log.debug('dereferenced:\n%s', self)
+            log.debug("dereferenced:\n%s", self)
 
     def mktns(self):
         """
         Make the schema's target namespace.
-        @return: The namespace representation of the schema's
-            targetNamespace value.
-        @rtype: (prefix, uri)
+
+        @return: namespace representation of the schema's targetNamespace
+            value.
+        @rtype: (prefix, URI)
+
         """
-        tns = [None, self.root.get('targetNamespace')]
-        if tns[1] is not None:
-            tns[0] = self.root.findPrefix(tns[1])
-        return tuple(tns)
+        tns = self.root.get("targetNamespace")
+        tns_prefix = None
+        if tns is not None:
+            tns_prefix = self.root.findPrefix(tns)
+        return tns_prefix, tns
 
     def build(self):
         """
-        Build the schema (object graph) using the root node
-        using the factory.
+        Build the schema (object graph) using the root node using the factory.
             - Build the graph.
             - Collate the children.
+
         """
         self.children = BasicFactory.build(self.root, self)
         collated = BasicFactory.collate(self.children)
@@ -255,11 +268,15 @@ class Schema(UnicodeMixin):
 
     def merge(self, schema):
         """
-        Merge the contents from the schema.  Only objects not already contained
-        in this schema's collections are merged.  This is to provide for bidirectional
-        import which produce cyclic includes.
+        Merge the schema contents.
+
+        Only objects not already contained in this schema's collections are
+        merged. This provides support for bidirectional imports producing
+        cyclic includes.
+
         @returns: self
         @rtype: L{Schema}
+
         """
         for item in schema.attributes.items():
             if item[0] in self.attributes:
@@ -291,95 +308,97 @@ class Schema(UnicodeMixin):
 
     def open_imports(self, options):
         """
-        Instruct all contained L{sxbasic.Import} children to import
-        the schema's which they reference.  The contents of the
-        imported schema are I{merged} in.
+        Instruct all contained L{sxbasic.Import} children to import all of
+        their referenced schemas. The imported schema contents are I{merged}
+        in.
+
         @param options: An options dictionary.
         @type options: L{options.Options}
+
         """
         for imp in self.imports:
             imported = imp.open(options)
             if imported is None:
                 continue
             imported.open_imports(options)
-            log.debug('imported:\n%s', imported)
+            log.debug("imported:\n%s", imported)
             self.merge(imported)
 
     def dereference(self):
-        """
-        Instruct all children to perform dereferencing.
-        """
+        """Instruct all children to perform dereferencing."""
         all = []
         indexes = {}
         for child in self.children:
             child.content(all)
-        deplist = DepList()
+        dependencies = {}
         for x in all:
             x.qualify()
             midx, deps = x.dependencies()
-            item = (x, tuple(deps))
-            deplist.add(item)
+            dependencies[x] = deps
             indexes[x] = midx
-        for x, deps in deplist.sort():
+        for x, deps in dependency_sort(dependencies):
             midx = indexes.get(x)
-            if midx is None: continue
+            if midx is None:
+                continue
             d = deps[midx]
-            log.debug('(%s) merging %s <== %s', self.tns[1], Repr(x), Repr(d))
+            log.debug("(%s) merging %s <== %s", self.tns[1], Repr(x), Repr(d))
             x.merge(d)
 
     def locate(self, ns):
         """
-        Find a schema by namespace.  Only the URI portion of
-        the namespace is compared to each schema's I{targetNamespace}.
-        The request is passed to the container.
+        Find a schema by namespace. Only the URI portion of the namespace is
+        compared to each schema's I{targetNamespace}. The request is passed to
+        the container.
+
         @param ns: A namespace.
-        @type ns: (prefix,URI)
+        @type ns: (prefix, URI)
         @return: The schema matching the namespace, else None.
         @rtype: L{Schema}
+
         """
         if self.container is not None:
             return self.container.locate(ns)
-        else:
-            return None
 
     def custom(self, ref, context=None):
         """
         Get whether the specified reference is B{not} an (xs) builtin.
+
         @param ref: A str or qref.
         @type ref: (str|qref)
         @return: True if B{not} a builtin, else False.
         @rtype: bool
+
         """
-        if ref is None:
-            return True
-        else:
-            return ( not self.builtin(ref, context) )
+        return ref is None or not self.builtin(ref, context)
 
     def builtin(self, ref, context=None):
         """
         Get whether the specified reference is an (xs) builtin.
+
         @param ref: A str or qref.
         @type ref: (str|qref)
         @return: True if builtin, else False.
         @rtype: bool
+
         """
-        w3 = 'http://www.w3.org'
+        w3 = "http://www.w3.org"
         try:
             if isqref(ref):
                 ns = ref[1]
-                return ( ref[0] in Factory.tags and ns.startswith(w3) )
+                return ref[0] in Factory.tags and ns.startswith(w3)
             if context is None:
                 context = self.root
             prefix = splitPrefix(ref)[0]
-            prefixes = context.findPrefixes(w3, 'startswith')
-            return ( prefix in prefixes and ref[0] in Factory.tags )
-        except:
+            prefixes = context.findPrefixes(w3, "startswith")
+            return prefix in prefixes and ref[0] in Factory.tags
+        except Exception:
             return False
 
     def instance(self, root, baseurl, options):
         """
-        Create and return an new schema object using the
-        specified I{root} and I{url}.
+        Create and return an new schema object using the specified I{root} and
+        I{URL}.
+
         @param root: A schema root node.
         @type root: L{sax.element.Element}
         @param baseurl: A base URL.
@@ -389,20 +408,21 @@ class Schema(UnicodeMixin):
         @return: The newly created schema object.
         @rtype: L{Schema}
         @note: This is only used by Import children.
+
         """
         return Schema(root, baseurl, options)
 
     def str(self, indent=0):
-        tab = '%*s'%(indent*3, '')
+        tab = "%*s" % (indent * 3, "")
         result = []
-        result.append('%s%s' % (tab, self.id))
-        result.append('%s(raw)' % tab)
-        result.append(self.root.str(indent+1))
-        result.append('%s(model)' % tab)
+        result.append("%s%s" % (tab, self.id))
+        result.append("%s(raw)" % (tab,))
+        result.append(self.root.str(indent + 1))
+        result.append("%s(model)" % (tab,))
         for c in self.children:
-            result.append(c.str(indent+1))
-        result.append('')
-        return '\n'.join(result)
+            result.append(c.str(indent + 1))
+        result.append("")
+        return "\n".join(result)
 
     def __repr__(self):
         return '<%s tns="%s"/>' % (self.id, self.tns[1])
diff --git a/suds/xsd/sxbase.py b/suds/xsd/sxbase.py
index 1ceb823..aed605f 100644
--- a/suds/xsd/sxbase.py
+++ b/suds/xsd/sxbase.py
@@ -1,22 +1,19 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
-"""
-The I{sxbase} module provides I{base} classes representing schema objects.
-"""
+"""I{Base} classes representing XSD schema objects."""
 
 from suds import *
 from suds.xsd import *
@@ -30,33 +27,37 @@ log = getLogger(__name__)
 class SchemaObject(UnicodeMixin):
     """
     A schema object is an extension to object with schema awareness.
+
     @ivar root: The XML root element.
     @type root: L{Element}
     @ivar schema: The schema containing this object.
     @type schema: L{schema.Schema}
-    @ivar form_qualified: A flag that indicates that @elementFormDefault
-        has a value of I{qualified}.
+    @ivar form_qualified: A flag indicating that @elementFormDefault has a
+        value of I{qualified}.
     @type form_qualified: boolean
-    @ivar nillable: A flag that indicates that @nillable
-        has a value of I{true}.
+    @ivar nillable: A flag indicating that @nillable has a value of I{true}.
     @type nillable: boolean
     @ivar default: The default value.
     @type default: object
     @ivar rawchildren: A list raw of all children.
     @type rawchildren: [L{SchemaObject},...]
+
     """
 
     @classmethod
     def prepend(cls, d, s, filter=Filter()):
         """
-        Prepend schema objects from B{s}ource list to
-        the B{d}estination list while applying the filter.
+        Prepend B{s}ource XSD schema objects to the B{d}estination list.
+
+        B{filter} is used to decide which objects to prepend and which to skip.
+
         @param d: The destination list.
         @type d: list
         @param s: The source list.
         @type s: list
-        @param filter: A filter that allows items to be prepended.
+        @param filter: A filter allowing items to be prepended.
         @type filter: L{Filter}
+
         """
         i = 0
         for x in s:
@@ -67,14 +68,17 @@ class SchemaObject(UnicodeMixin):
     @classmethod
     def append(cls, d, s, filter=Filter()):
         """
-        Append schema objects from B{s}ource list to
-        the B{d}estination list while applying the filter.
+        Append B{s}ource XSD schema objects to the B{d}estination list.
+
+        B{filter} is used to decide which objects to append and which to skip.
+
         @param d: The destination list.
         @type d: list
         @param s: The source list.
         @type s: list
         @param filter: A filter that allows items to be appended.
         @type filter: L{Filter}
+
         """
         for item in s:
             if item in filter:
@@ -86,28 +90,31 @@ class SchemaObject(UnicodeMixin):
         @type schema: L{schema.Schema}
         @param root: The XML root node.
         @type root: L{Element}
+
         """
         self.schema = schema
         self.root = root
         self.id = objid(self)
-        self.name = root.get('name')
+        self.name = root.get("name")
         self.qname = (self.name, schema.tns[1])
-        self.min = root.get('minOccurs')
-        self.max = root.get('maxOccurs')
-        self.type = root.get('type')
-        self.ref = root.get('ref')
+        self.min = root.get("minOccurs")
+        self.max = root.get("maxOccurs")
+        self.type = root.get("type")
+        self.ref = root.get("ref")
         self.form_qualified = schema.form_qualified
         self.nillable = False
-        self.default = root.get('default')
+        self.default = root.get("default")
         self.rawchildren = []
 
     def attributes(self, filter=Filter()):
         """
         Get only the attribute content.
+
         @param filter: A filter to constrain the result.
         @type filter: L{Filter}
-        @return: A list of tuples (attr, ancestry)
+        @return: A list of (attr, ancestry) tuples.
         @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
+
         """
         result = []
         for child, ancestry in self:
@@ -118,10 +125,12 @@ class SchemaObject(UnicodeMixin):
     def children(self, filter=Filter()):
         """
         Get only the I{direct} or non-attribute content.
+
         @param filter: A filter to constrain the result.
         @type filter: L{Filter}
         @return: A list tuples: (child, ancestry)
         @rtype: [(L{SchemaObject}, [L{SchemaObject},..]),..]
+
         """
         result = []
         for child, ancestry in self:
@@ -132,10 +141,12 @@ class SchemaObject(UnicodeMixin):
     def get_attribute(self, name):
         """
         Get (find) an attribute by name.
+
         @param name: A attribute name.
         @type name: str
         @return: A tuple: the requested (attribute, ancestry).
         @rtype: (L{SchemaObject}, [L{SchemaObject},..])
+
         """
         for child, ancestry in self.attributes():
             if child.name == name:
@@ -145,10 +156,12 @@ class SchemaObject(UnicodeMixin):
     def get_child(self, name):
         """
         Get (find) a I{non-attribute} child by name.
+
         @param name: A child name.
         @type name: str
         @return: A tuple: the requested (child, ancestry).
         @rtype: (L{SchemaObject}, [L{SchemaObject},..])
+
         """
         for child, ancestry in self.children():
             if child.any() or child.name == name:
@@ -158,10 +171,12 @@ class SchemaObject(UnicodeMixin):
     def namespace(self, prefix=None):
         """
         Get this property's namespace.
+
         @param prefix: The default prefix.
         @type prefix: str
-        @return: The schema's target namespace
-        @rtype: (I{prefix},I{URI})
+        @return: The schema's target namespace.
+        @rtype: (I{prefix}, I{URI})
+
         """
         ns = self.schema.tns
         if ns[0] is None:
@@ -174,29 +189,35 @@ class SchemaObject(UnicodeMixin):
     def multi_occurrence(self):
         """
         Get whether the node has multiple occurrences, i.e. is a I{collection}.
-        @return: True if it has, False if it has 1 occurrence at most.
+
+        @return: True if it has, False if it has at most 1 occurrence.
         @rtype: boolean
+
         """
         max = self.max
         if max is None:
             return False
         if max.isdigit():
             return int(max) > 1
-        return max == 'unbounded'
+        return max == "unbounded"
 
     def optional(self):
         """
         Get whether this type is optional.
-        @return: True if optional, else False
+
+        @return: True if optional, else False.
         @rtype: boolean
+
         """
-        return self.min == '0'
+        return self.min == "0"
 
     def required(self):
         """
         Get whether this type is required.
-        @return: True if required, else False
+
+        @return: True if required, else False.
         @rtype: boolean
+
         """
         return not self.optional()
 
@@ -204,10 +225,11 @@ class SchemaObject(UnicodeMixin):
         """
         Resolve the node's type reference and return the referenced type node.
 
-        Only schema objects that actually support 'having a type' custom
+        Only XSD schema objects that actually support 'having a type' custom
         implement this interface while others simply resolve as themselves.
+
         @param nobuiltin: Flag indicating whether resolving to an external XSD
-            builtin type should not be allowed.
+            built-in type should not be allowed.
         @return: The resolved (true) type.
         @rtype: L{SchemaObject}
         """
@@ -215,93 +237,112 @@ class SchemaObject(UnicodeMixin):
 
     def sequence(self):
         """
-        Get whether this is a <xs:sequence/>.
-        @return: True if <xs:sequence/>, else False
+        Get whether this is an <xsd:sequence/>.
+
+        @return: True if <xsd:sequence/>, else False.
         @rtype: boolean
+
         """
         return False
 
     def xslist(self):
         """
-        Get whether this is a <xs:list/>.
-        @return: True if any, else False
+        Get whether this is an <xsd:list/>.
+
+        @return: True if <xsd:list/>, else False.
         @rtype: boolean
+
         """
         return False
 
     def all(self):
         """
-        Get whether this is an <xs:all/>.
-        @return: True if any, else False
+        Get whether this is an <xsd:all/>.
+
+        @return: True if <xsd:all/>, else False.
         @rtype: boolean
+
         """
         return False
 
     def choice(self):
         """
-        Get whether this is a <xs:choice/>.
-        @return: True if any, else False
+        Get whether this is an <xsd:choice/>.
+
+        @return: True if <xsd:choice/>, else False.
         @rtype: boolean
+
         """
         return False
 
     def any(self):
         """
-        Get whether this is an <xs:any/>.
-        @return: True if any, else False
+        Get whether this is an <xsd:any/>.
+
+        @return: True if <xsd:any/>, else False.
         @rtype: boolean
+
         """
         return False
 
     def builtin(self):
         """
-        Get whether this is a schema-instance (xs) type.
-        @return: True if any, else False
+        Get whether this is a built-in schema-instance XSD type.
+
+        @return: True if a built-in type, else False.
         @rtype: boolean
+
         """
         return False
 
     def enum(self):
         """
         Get whether this is a simple-type containing an enumeration.
-        @return: True if any, else False
+
+        @return: True if enumeration, else False.
         @rtype: boolean
+
         """
         return False
 
     def isattr(self):
         """
         Get whether the object is a schema I{attribute} definition.
+
         @return: True if an attribute, else False.
         @rtype: boolean
+
         """
         return False
 
     def extension(self):
         """
         Get whether the object is an extension of another type.
+
         @return: True if an extension, else False.
         @rtype: boolean
+
         """
         return False
 
     def restriction(self):
         """
         Get whether the object is an restriction of another type.
-        @return: True if an restriction, else False.
+
+        @return: True if a restriction, else False.
         @rtype: boolean
+
         """
         return False
 
     def mixed(self):
-        """
-        Get whether this I{mixed} content.
-        """
+        """Get whether the object has I{mixed} content."""
         return False
 
     def find(self, qref, classes=()):
         """
         Find a referenced type in self or children.
+
         @param qref: A qualified reference.
         @type qref: qref
         @param classes: A list of classes used to qualify the match.
@@ -309,6 +350,7 @@ class SchemaObject(UnicodeMixin):
         @return: The referenced type.
         @rtype: L{SchemaObject}
         @see: L{qualify()}
+
         """
         if not len(classes):
             classes = (self.__class__,)
@@ -318,48 +360,65 @@ class SchemaObject(UnicodeMixin):
             p = c.find(qref, classes)
             if p is not None:
                 return p
-        return None
 
     def translate(self, value, topython=True):
         """
-        Translate a value (type) to/from a Python type.
+        Translate between an XSD type values and Python objects.
+
+        When converting a Python object to an XSD type value the operation may
+        return any Python object whose string representation matches the
+        desired XSD type value.
+
         @param value: A value to translate.
+        @type value: str if topython is True; any Python object otherwise
+        @param topython: Flag indicating the translation direction.
+        @type topython: bool
         @return: The converted I{language} type.
+
         """
         return value
 
     def childtags(self):
         """
         Get a list of valid child tag names.
+
         @return: A list of child tag names.
         @rtype: [str,...]
+
         """
         return ()
 
     def dependencies(self):
         """
         Get a list of dependencies for dereferencing.
+
         @return: A merge dependency index and a list of dependencies.
         @rtype: (int, [L{SchemaObject},...])
+
         """
         return None, []
 
     def autoqualified(self):
         """
         The list of I{auto} qualified attribute values.
+
         Qualification means to convert values into I{qref}.
-        @return: A list of attibute names.
+
+        @return: A list of attribute names.
         @rtype: list
+
         """
-        return ['type', 'ref']
+        return ["type", "ref"]
 
     def qualify(self):
         """
-        Convert attribute values, that are references to other
-        objects, into I{qref}.  Qualified using the default document namespace.
-        Since many WSDLs are written improperly: when the document does
-        not define its default namespace, the schema target namespace is used
-        to qualify references.
+        Convert reference attribute values into a I{qref}.
+
+        Constructed I{qref} uses the default document namespace. Since many
+        WSDL schemas are written improperly: when the document does not define
+        its default namespace, the schema target namespace is used to qualify
+        references.
+
         """
         defns = self.root.defaultNamespace()
         if Namespace.none(defns):
@@ -371,22 +430,14 @@ class SchemaObject(UnicodeMixin):
             if isqref(ref):
                 continue
             qref = qualify(ref, self.root, defns)
-            log.debug('%s, convert %s="%s" to %s', self.id, a, ref, qref)
+            log.debug("%s, convert %s='%s' to %s", self.id, a, ref, qref)
             setattr(self, a, qref)
 
     def merge(self, other):
-        """
-        Merge another object as needed.
-        """
+        """Merge another object as needed."""
         other.qualify()
-        for n in ('name',
-                  'qname',
-                  'min',
-                  'max',
-                  'default',
-                  'type',
-                  'nillable',
-                  'form_qualified'):
+        for n in ("default", "max", "min", "name", "nillable", "qname",
+                "type"):
             if getattr(self, n) is not None:
                 continue
             v = getattr(other, n)
@@ -397,6 +448,7 @@ class SchemaObject(UnicodeMixin):
     def content(self, collection=None, filter=Filter(), history=None):
         """
         Get a I{flattened} list of this node's contents.
+
         @param collection: A list to fill.
         @type collection: list
         @param filter: A filter used to constrain the result.
@@ -405,6 +457,7 @@ class SchemaObject(UnicodeMixin):
         @type history: list
         @return: The filled list.
         @rtype: list
+
         """
         if collection is None:
             collection = []
@@ -416,24 +469,27 @@ class SchemaObject(UnicodeMixin):
         if self in filter:
             collection.append(self)
         for c in self.rawchildren:
-            c.content(collection, filter, history[:])
+            c.content(collection, filter, history)
+        history.pop()
         return collection
 
     def str(self, indent=0, history=None):
         """
         Get a string representation of this object.
+
         @param indent: The indent.
         @type indent: int
         @return: A string.
         @rtype: str
+
         """
         if history is None:
             history = []
         if self in history:
-            return '%s ...' % Repr(self)
+            return "%s ..." % Repr(self)
         history.append(self)
-        tab = '%*s'%(indent*3, '')
-        result = ['%s<%s' % (tab, self.id)]
+        tab = "%*s" % (indent * 3, "")
+        result = ["%s<%s" % (tab, self.id)]
         for n in self.description():
             if not hasattr(self, n):
                 continue
@@ -442,23 +498,25 @@ class SchemaObject(UnicodeMixin):
                 continue
             result.append(' %s="%s"' % (n, v))
         if len(self):
-            result.append('>')
+            result.append(">")
             for c in self.rawchildren:
-                result.append('\n')
+                result.append("\n")
                 result.append(c.str(indent+1, history[:]))
                 if c.isattr():
-                    result.append('@')
-            result.append('\n%s' % tab)
-            result.append('</%s>' % self.__class__.__name__)
+                    result.append("@")
+            result.append("\n%s" % (tab,))
+            result.append("</%s>" % (self.__class__.__name__,))
         else:
-            result.append(' />')
-        return ''.join(result)
+            result.append(" />")
+        return "".join(result)
 
     def description(self):
         """
-        Get the names used for str() and repr() description.
+        Get the names used for repr() and str() description.
+
         @return:  A dictionary of relevant attributes.
         @rtype: [str,...]
+
         """
         return ()
 
@@ -467,7 +525,7 @@ class SchemaObject(UnicodeMixin):
 
     def __repr__(self):
         s = []
-        s.append('<%s' % self.id)
+        s.append("<%s" % (self.id,))
         for n in self.description():
             if not hasattr(self, n):
                 continue
@@ -475,19 +533,21 @@ class SchemaObject(UnicodeMixin):
             if v is None:
                 continue
             s.append(' %s="%s"' % (n, v))
-        s.append(' />')
-        return ''.join(s)
+        s.append(" />")
+        return "".join(s)
 
     def __len__(self):
         n = 0
-        for x in self: n += 1
+        for x in self:
+            n += 1
         return n
 
     def __iter__(self):
         return Iter(self)
 
     def __getitem__(self, index):
-        """Returns a contained schema object referenced by its 0-based index.
+        """
+        Returns a contained schema object referenced by its 0-based index.
 
         Returns None if such an object does not exist.
 
@@ -501,20 +561,24 @@ class SchemaObject(UnicodeMixin):
 
 class Iter:
     """
-    The content iterator - used to iterate the L{Content} children.  The
-    iterator provides a I{view} of the children that is free of container
-    elements such as <sequence/> and <choice/>.
+    The content iterator - used to iterate the L{Content} children.
+
+    The iterator provides a I{view} of the children that is free of container
+    elements such as <xsd::all/>, <xsd:choice/> or <xsd:sequence/>.
+
     @ivar stack: A stack used to control nesting.
     @type stack: list
+
     """
 
     class Frame:
-        """ A content iterator frame. """
+        """A content iterator frame."""
 
         def __init__(self, sx):
             """
             @param sx: A schema object.
             @type sx: L{SchemaObject}
+
             """
             self.sx = sx
             self.items = sx.rawchildren
@@ -523,8 +587,10 @@ class Iter:
         def next(self):
             """
             Get the I{next} item in the frame's collection.
+
             @return: The next item or None
             @rtype: L{SchemaObject}
+
             """
             if self.index < len(self.items):
                 result = self.items[self.index]
@@ -535,6 +601,7 @@ class Iter:
         """
         @param sx: A schema object.
         @type sx: L{SchemaObject}
+
         """
         self.stack = []
         self.push(sx)
@@ -542,41 +609,47 @@ class Iter:
     def push(self, sx):
         """
         Create a frame and push the specified object.
+
         @param sx: A schema object to push.
         @type sx: L{SchemaObject}
+
         """
         self.stack.append(Iter.Frame(sx))
 
     def pop(self):
         """
         Pop the I{top} frame.
+
         @return: The popped frame.
         @rtype: L{Frame}
         @raise StopIteration: when stack is empty.
+
         """
-        if len(self.stack):
+        if self.stack:
             return self.stack.pop()
-        else:
-            raise StopIteration()
+        raise StopIteration()
 
     def top(self):
         """
         Get the I{top} frame.
+
         @return: The top frame.
         @rtype: L{Frame}
         @raise StopIteration: when stack is empty.
+
         """
-        if len(self.stack):
+        if self.stack:
             return self.stack[-1]
-        else:
-            raise StopIteration()
+        raise StopIteration()
 
     def next(self):
         """
         Get the next item.
+
         @return: A tuple: the next (child, ancestry).
         @rtype: (L{SchemaObject}, [L{SchemaObject},..])
         @raise StopIteration: A the end.
+
         """
         frame = self.top()
         while True:
@@ -595,14 +668,13 @@ class Iter:
 
 
 class XBuiltin(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:*/> node.
-    """
+    """Represents a built-in XSD schema <xsd:*/> node."""
 
     def __init__(self, schema, name):
         """
         @param schema: The containing schema.
         @type schema: L{schema.Schema}
+
         """
         root = Element(name)
         SchemaObject.__init__(self, schema, root)
@@ -617,28 +689,29 @@ class XBuiltin(SchemaObject):
 
 
 class Content(SchemaObject):
-    """
-    This class represents those schema objects that represent
-    real XML document content.
-    """
+    """XSD schema objects representing real XML document content."""
     pass
 
 
 class NodeFinder:
     """
-    Find nodes based on flexable criteria.  The I{matcher}
-    may be any object that implements a match(n) method.
+    Find nodes based on flexable criteria.
+
+    I{matcher} may be any object implementing a match(n) method.
+
     @ivar matcher: An object used as criteria for match.
     @type matcher: I{any}.match(n)
     @ivar limit: Limit the number of matches.  0=unlimited.
     @type limit: int
+
     """
     def __init__(self, matcher, limit=0):
         """
         @param matcher: An object used as criteria for match.
         @type matcher: I{any}.match(n)
-        @param limit: Limit the number of matches.  0=unlimited.
+        @param limit: Limit the number of matches. 0=unlimited.
         @type limit: int
+
         """
         self.matcher = matcher
         self.limit = limit
@@ -646,10 +719,12 @@ class NodeFinder:
     def find(self, node, list):
         """
         Traverse the tree looking for matches.
+
         @param node: A node to match on.
         @type node: L{SchemaObject}
         @param list: A list to fill.
         @type list: list
+
         """
         if self.matcher.match(node):
             list.append(node)
diff --git a/suds/xsd/sxbasic.py b/suds/xsd/sxbasic.py
index f605088..66f4af5 100644
--- a/suds/xsd/sxbasic.py
+++ b/suds/xsd/sxbasic.py
@@ -1,31 +1,28 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
-"""
-The I{sxbasic} module provides classes that represent
-I{basic} schema objects.
-"""
+"""Classes representing I{basic} XSD schema objects."""
 
 from suds import *
-from suds.xsd import *
-from suds.xsd.sxbase import *
-from suds.xsd.query import *
+from suds.reader import DocumentReader
 from suds.sax import Namespace
 from suds.transport import TransportError
-from suds.reader import DocumentReader
+from suds.xsd import *
+from suds.xsd.query import *
+from suds.xsd.sxbase import *
+
 from urlparse import urljoin
 
 from logging import getLogger
@@ -33,17 +30,13 @@ log = getLogger(__name__)
 
 
 class RestrictionMatcher:
-    """
-    For use with L{NodeFinder} to match restriction.
-    """
+    """For use with L{NodeFinder} to match restriction."""
     def match(self, n):
         return isinstance(n, Restriction)
 
 
 class TypedContent(Content):
-    """
-    Represents any I{typed} content.
-    """
+    """Represents any I{typed} content."""
 
     def __init__(self, *args, **kwargs):
         Content.__init__(self, *args, **kwargs)
@@ -55,10 +48,12 @@ class TypedContent(Content):
 
         Returns self if the type is defined locally, e.g. as a <complexType>
         subnode. Otherwise returns the referenced external node.
-        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+
+        @param nobuiltin: Flag indicating whether resolving to XSD built-in
             types should not be allowed.
         @return: The resolved (true) type.
         @rtype: L{SchemaObject}
+
         """
         cached = self.resolved_cache.get(nobuiltin)
         if cached is not None:
@@ -70,23 +65,23 @@ class TypedContent(Content):
     def __resolve_type(self, nobuiltin=False):
         """
         Private resolve() worker without any result caching.
-        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+
+        @param nobuiltin: Flag indicating whether resolving to XSD built-in
             types should not be allowed.
         @return: The resolved (true) type.
         @rtype: L{SchemaObject}
 
-        Implementation note:
-          Note that there is no need for a recursive implementation here since
-        a node can reference an external type node but there is no way using
-        WSDL to then make that type node actually be a reference to a different
-        type node.
         """
+        # There is no need for a recursive implementation here since a node can
+        # reference an external type node but XSD specification explicitly
+        # states that that external node must not be a reference to yet another
+        # node.
         qref = self.qref()
         if qref is None:
             return self
         query = TypeQuery(qref)
         query.history = [self]
-        log.debug('%s, resolving: %s\n using:%s', self.id, qref, query)
+        log.debug("%s, resolving: %s\n using:%s", self.id, qref, query)
         resolved = query.execute(self.schema)
         if resolved is None:
             log.debug(self.schema)
@@ -98,11 +93,14 @@ class TypedContent(Content):
     def qref(self):
         """
         Get the I{type} qualified reference to the referenced XSD type.
-        This method takes into account simple types defined through
-        restriction which are detected by determining that self is simple
-        (len=0) and by finding a restriction child.
+
+        This method takes into account simple types defined through restriction
+        which are detected by determining that self is simple (len == 0) and by
+        finding a restriction child.
+
         @return: The I{type} qualified reference.
         @rtype: qref
+
         """
         qref = self.type
         if qref is None and len(self) == 0:
@@ -110,24 +108,26 @@ class TypedContent(Content):
             m = RestrictionMatcher()
             finder = NodeFinder(m, 1)
             finder.find(self, ls)
-            if len(ls):
+            if ls:
                 return ls[0].ref
         return qref
 
 
 class Complex(SchemaObject):
     """
-    Represents an (XSD) schema <xs:complexType/> node.
+    Represents an XSD schema <xsd:complexType/> node.
+
     @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
+
     """
 
     def childtags(self):
-        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
-            'complexContent', 'simpleContent', 'any', 'group'
+        return ("all", "any", "attribute", "attributeGroup", "choice",
+            "complexContent", "group", "sequence", "simpleContent")
 
     def description(self):
-        return ('name',)
+        return ("name",)
 
     def extension(self):
         for c in self.rawchildren:
@@ -144,13 +144,15 @@ class Complex(SchemaObject):
 
 class Group(SchemaObject):
     """
-    Represents an (XSD) schema <xs:group/> node.
+    Represents an XSD schema <xsd:group/> node.
+
     @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
+
     """
 
     def childtags(self):
-        return 'sequence', 'all', 'choice'
+        return "all", "choice", "sequence"
 
     def dependencies(self):
         deps = []
@@ -170,18 +172,20 @@ class Group(SchemaObject):
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return 'name', 'ref'
+        return "name", "ref"
 
 
 class AttributeGroup(SchemaObject):
     """
-    Represents an (XSD) schema <xs:attributeGroup/> node.
+    Represents an XSD schema <xsd:attributeGroup/> node.
+
     @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
+
     """
 
     def childtags(self):
-        return 'attribute', 'attributeGroup'
+        return "attribute", "attributeGroup"
 
     def dependencies(self):
         deps = []
@@ -201,16 +205,14 @@ class AttributeGroup(SchemaObject):
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return 'name', 'ref'
+        return "name", "ref"
 
 
 class Simple(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:simpleType/> node.
-    """
+    """Represents an XSD schema <xsd:simpleType/> node."""
 
     def childtags(self):
-        return 'restriction', 'any', 'list'
+        return "any", "list", "restriction"
 
     def enum(self):
         for child, ancestry in self.children():
@@ -222,7 +224,7 @@ class Simple(SchemaObject):
         return len(self)
 
     def description(self):
-        return ('name',)
+        return ("name",)
 
     def extension(self):
         for c in self.rawchildren:
@@ -238,31 +240,27 @@ class Simple(SchemaObject):
 
 
 class List(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:list/> node.
-    """
+    """Represents an XSD schema <xsd:list/> node."""
 
     def childtags(self):
         return ()
 
     def description(self):
-        return ('name',)
+        return ("name",)
 
     def xslist(self):
         return True
 
 
 class Restriction(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:restriction/> node.
-    """
+    """Represents an XSD schema <xsd:restriction/> node."""
 
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
-        self.ref = root.get('base')
+        self.ref = root.get("base")
 
     def childtags(self):
-        return 'enumeration', 'attribute', 'attributeGroup'
+        return "attribute", "attributeGroup", "enumeration"
 
     def dependencies(self):
         deps = []
@@ -287,52 +285,39 @@ class Restriction(SchemaObject):
         self.prepend(self.rawchildren, other.rawchildren, filter)
 
     def description(self):
-        return ('ref',)
+        return ("ref",)
 
 
 class Collection(SchemaObject):
-    """
-    Represents an (XSD) schema collection node:
-        - sequence
-        - choice
-        - all
-    """
+    """Represents an XSD schema collection (a.k.a. order indicator) node."""
 
     def childtags(self):
-        return 'element', 'sequence', 'all', 'choice', 'any', 'group'
-
-
-class Sequence(Collection):
-    """
-    Represents an (XSD) schema <xs:sequence/> node.
-    """
-    def sequence(self):
-        return True
+        return "all", "any", "choice", "element", "group", "sequence"
 
 
 class All(Collection):
-    """
-    Represents an (XSD) schema <xs:all/> node.
-    """
+    """Represents an XSD schema <xsd:all/> node."""
     def all(self):
         return True
 
 
 class Choice(Collection):
-    """
-    Represents an (XSD) schema <xs:choice/> node.
-    """
+    """Represents an XSD schema <xsd:choice/> node."""
     def choice(self):
         return True
 
 
+class Sequence(Collection):
+    """Represents an XSD schema <xsd:sequence/> node."""
+    def sequence(self):
+        return True
+
+
 class ComplexContent(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:complexContent/> node.
-    """
+    """Represents an XSD schema <xsd:complexContent/> node."""
 
     def childtags(self):
-        return 'attribute', 'attributeGroup', 'extension', 'restriction'
+        return "attribute", "attributeGroup", "extension", "restriction"
 
     def extension(self):
         for c in self.rawchildren:
@@ -348,12 +333,10 @@ class ComplexContent(SchemaObject):
 
 
 class SimpleContent(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:simpleContent/> node.
-    """
+    """Represents an XSD schema <xsd:simpleContent/> node."""
 
     def childtags(self):
-        return 'extension', 'restriction'
+        return "extension", "restriction"
 
     def extension(self):
         for c in self.rawchildren:
@@ -372,52 +355,54 @@ class SimpleContent(SchemaObject):
 
 
 class Enumeration(Content):
-    """
-    Represents an (XSD) schema <xs:enumeration/> node.
-    """
+    """Represents an XSD schema <xsd:enumeration/> node."""
 
     def __init__(self, schema, root):
         Content.__init__(self, schema, root)
-        self.name = root.get('value')
+        self.name = root.get("value")
 
     def description(self):
-        return ('name',)
+        return ("name",)
 
     def enum(self):
         return True
 
 
 class Element(TypedContent):
-    """
-    Represents an (XSD) schema <xs:element/> node.
-    """
+    """Represents an XSD schema <xsd:element/> node."""
 
     def __init__(self, schema, root):
         TypedContent.__init__(self, schema, root)
-        a = root.get('form')
-        if a is not None:
-            self.form_qualified = ( a == 'qualified' )
-        a = self.root.get('nillable')
-        if a is not None:
-            self.nillable = ( a in ('1', 'true') )
+        is_reference = self.ref is not None
+        is_top_level = root.parent is schema.root
+        if is_reference or is_top_level:
+            self.form_qualified = True
+        else:
+            form = root.get("form")
+            if form is not None:
+                self.form_qualified = (form == "qualified")
+        nillable = self.root.get("nillable")
+        if nillable is not None:
+            self.nillable = (nillable in ("1", "true"))
         self.implany()
 
     def implany(self):
         """
-        Set the type as any when implicit.
-        An implicit <xs:any/> is when an element has not
-        body and no type defined.
+        Set the type to <xsd:any/> when implicit.
+
+        An element has an implicit <xsd:any/> type when it has no body and no
+        explicitly defined type.
+
         @return: self
         @rtype: L{Element}
+
         """
-        if self.type is None and \
-            self.ref is None and \
-            self.root.isempty():
-                self.type = self.anytype()
+        if self.type is None and self.ref is None and self.root.isempty():
+            self.type = self.anytype()
         return self
 
     def childtags(self):
-        return 'attribute', 'simpleType', 'complexType', 'any'
+        return "any", "attribute", "complexType", "simpleType"
 
     def extension(self):
         for c in self.rawchildren:
@@ -445,16 +430,16 @@ class Element(TypedContent):
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return 'name', 'ref', 'type'
+        return "name", "ref", "type"
 
     def anytype(self):
-        """ create an xsd:anyType reference """
+        """Create an xsd:anyType reference."""
         p, u = Namespace.xsdns
         mp = self.root.findPrefix(u)
         if mp is None:
             mp = p
             self.root.addPrefix(p, u)
-        return ':'.join((mp, 'anyType'))
+        return ":".join((mp, "anyType"))
 
     def namespace(self, prefix=None):
         """
@@ -467,6 +452,7 @@ class Element(TypedContent):
         @type prefix: str
         @return: The schema element's target namespace
         @rtype: (I{prefix},I{URI})
+
         """
         e = self.__deref()
         if e is not None:
@@ -485,17 +471,15 @@ class Element(TypedContent):
 
 
 class Extension(SchemaObject):
-    """
-    Represents an (XSD) schema <xs:extension/> node.
-    """
+    """Represents an XSD schema <xsd:extension/> node."""
 
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
-        self.ref = root.get('base')
+        self.ref = root.get("base")
 
     def childtags(self):
-        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
-            'group'
+        return ("all", "attribute", "attributeGroup", "choice", "group",
+            "sequence")
 
     def dependencies(self):
         deps = []
@@ -520,12 +504,13 @@ class Extension(SchemaObject):
         return self.ref is not None
 
     def description(self):
-        return ('ref',)
+        return ("ref",)
 
 
 class Import(SchemaObject):
     """
-    Represents an (XSD) schema <xs:import/> node.
+    Represents an XSD schema <xsd:import/> node.
+
     @cvar locations: A dictionary of namespace locations.
     @type locations: dict
     @ivar ns: The imported namespace.
@@ -534,6 +519,7 @@ class Import(SchemaObject):
     @type location: namespace-uri
     @ivar opened: Opened and I{imported} flag.
     @type opened: boolean
+
     """
 
     locations = {}
@@ -542,12 +528,15 @@ class Import(SchemaObject):
     def bind(cls, ns, location=None):
         """
         Bind a namespace to a schema location (URI).
-        This is used for imports that don't specify a schemaLocation.
+
+        This is used for imports that do not specify a schemaLocation.
+
         @param ns: A namespace-uri.
         @type ns: str
-        @param location: The (optional) schema location for the
-            namespace.  (default=ns).
+        @param location: The (optional) schema location for the namespace.
+            (default=ns)
         @type location: str
+
         """
         if location is None:
             location = ns
@@ -555,150 +544,156 @@ class Import(SchemaObject):
 
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
-        self.ns = (None, root.get('namespace'))
-        self.location = root.get('schemaLocation')
+        self.ns = (None, root.get("namespace"))
+        self.location = root.get("schemaLocation")
         if self.location is None:
             self.location = self.locations.get(self.ns[1])
         self.opened = False
 
     def open(self, options):
         """
-        Open and import the refrenced schema.
+        Open and import the referenced schema.
+
         @param options: An options dictionary.
         @type options: L{options.Options}
         @return: The referenced schema.
         @rtype: L{Schema}
+
         """
         if self.opened:
             return
         self.opened = True
-        log.debug('%s, importing ns="%s", location="%s"', self.id, self.ns[1], self.location)
+        log.debug("%s, importing ns='%s', location='%s'", self.id, self.ns[1],
+            self.location)
         result = self.locate()
         if result is None:
             if self.location is None:
-                log.debug('imported schema (%s) not-found', self.ns[1])
+                log.debug("imported schema (%s) not-found", self.ns[1])
             else:
                 result = self.download(options)
-        log.debug('imported:\n%s', result)
+        log.debug("imported:\n%s", result)
         return result
 
     def locate(self):
-        """ find the schema locally """
+        """Find the schema locally."""
         if self.ns[1] != self.schema.tns[1]:
             return self.schema.locate(self.ns)
 
     def download(self, options):
-        """ download the schema """
+        """Download the schema."""
         url = self.location
         try:
-            if '://' not in url:
+            if "://" not in url:
                 url = urljoin(self.schema.baseurl, url)
             reader = DocumentReader(options)
             d = reader.open(url)
             root = d.root()
-            root.set('url', url)
+            root.set("url", url)
             return self.schema.instance(root, url, options)
         except TransportError:
-            msg = 'imported schema (%s) at (%s), failed' % (self.ns[1], url)
-            log.error('%s, %s', self.id, msg, exc_info=True)
+            msg = "imported schema (%s) at (%s), failed" % (self.ns[1], url)
+            log.error("%s, %s", self.id, msg, exc_info=True)
             raise Exception(msg)
 
     def description(self):
-        return 'ns', 'location'
+        return "ns", "location"
 
 
 class Include(SchemaObject):
     """
-    Represents an (XSD) schema <xs:include/> node.
+    Represents an XSD schema <xsd:include/> node.
+
     @ivar location: The (optional) location.
     @type location: namespace-uri
     @ivar opened: Opened and I{imported} flag.
     @type opened: boolean
+
     """
 
     locations = {}
 
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
-        self.location = root.get('schemaLocation')
+        self.location = root.get("schemaLocation")
         if self.location is None:
             self.location = self.locations.get(self.ns[1])
         self.opened = False
 
     def open(self, options):
         """
-        Open and include the refrenced schema.
+        Open and include the referenced schema.
+
         @param options: An options dictionary.
         @type options: L{options.Options}
         @return: The referenced schema.
         @rtype: L{Schema}
+
         """
         if self.opened:
             return
         self.opened = True
-        log.debug('%s, including location="%s"', self.id, self.location)
+        log.debug("%s, including location='%s'", self.id, self.location)
         result = self.download(options)
-        log.debug('included:\n%s', result)
+        log.debug("included:\n%s", result)
         return result
 
     def download(self, options):
-        """ download the schema """
+        """Download the schema."""
         url = self.location
         try:
-            if '://' not in url:
+            if "://" not in url:
                 url = urljoin(self.schema.baseurl, url)
             reader = DocumentReader(options)
             d = reader.open(url)
             root = d.root()
-            root.set('url', url)
+            root.set("url", url)
             self.__applytns(root)
             return self.schema.instance(root, url, options)
         except TransportError:
-            msg = 'include schema at (%s), failed' % url
-            log.error('%s, %s', self.id, msg, exc_info=True)
+            msg = "include schema at (%s), failed" % url
+            log.error("%s, %s", self.id, msg, exc_info=True)
             raise Exception(msg)
 
     def __applytns(self, root):
-        """ make sure included schema has same tns. """
-        TNS = 'targetNamespace'
+        """Make sure included schema has the same target namespace."""
+        TNS = "targetNamespace"
         tns = root.get(TNS)
         if tns is None:
             tns = self.schema.tns[1]
             root.set(TNS, tns)
         else:
             if self.schema.tns[1] != tns:
-                raise Exception, '%s mismatch' % TNS
-
+                raise Exception, "%s mismatch" % TNS
 
     def description(self):
-        return 'location'
+        return "location"
 
 
 class Attribute(TypedContent):
-    """
-    Represents an (XSD) <attribute/> node.
-    """
+    """Represents an XSD schema <attribute/> node."""
 
     def __init__(self, schema, root):
         TypedContent.__init__(self, schema, root)
-        self.use = root.get('use', default='')
+        self.use = root.get("use", default="")
 
     def childtags(self):
-        return ('restriction',)
+        return ("restriction",)
 
     def isattr(self):
         return True
 
     def get_default(self):
         """
-        Gets the <xs:attribute default=""/> attribute value.
+        Gets the <xsd:attribute default=""/> attribute value.
+
         @return: The default value for the attribute
         @rtype: str
+
         """
-        return self.root.get('default', default='')
+        return self.root.get("default", default="")
 
     def optional(self):
-        return self.use != 'required'
+        return self.use != "required"
 
     def dependencies(self):
         deps = []
@@ -714,23 +709,21 @@ class Attribute(TypedContent):
         return midx, deps
 
     def description(self):
-        return 'name', 'ref', 'type'
+        return "name", "ref", "type"
 
 
 class Any(Content):
-    """
-    Represents an (XSD) <any/> node.
-    """
+    """Represents an XSD schema <any/> node."""
 
     def get_child(self, name):
         root = self.root.clone()
-        root.set('note', 'synthesized (any) child')
+        root.set("note", "synthesized (any) child")
         child = Any(self.schema, root)
         return child, []
 
     def get_attribute(self, name):
         root = self.root.clone()
-        root.set('note', 'synthesized (any) attribute')
+        root.set("note", "synthesized (any) attribute")
         attribute = Any(self.schema, root)
         return attribute, []
 
@@ -742,37 +735,40 @@ class Factory:
     """
     @cvar tags: A factory to create object objects based on tag.
     @type tags: {tag:fn,}
+
     """
 
     tags = {
-        'import' : Import,
-        'include' : Include,
-        'complexType' : Complex,
-        'group' : Group,
-        'attributeGroup' : AttributeGroup,
-        'simpleType' : Simple,
-        'list' : List,
-        'element' : Element,
-        'attribute' : Attribute,
-        'sequence' : Sequence,
-        'all' : All,
-        'choice' : Choice,
-        'complexContent' : ComplexContent,
-        'simpleContent' : SimpleContent,
-        'restriction' : Restriction,
-        'enumeration' : Enumeration,
-        'extension' : Extension,
-        'any' : Any,
+        "all": All,
+        "any": Any,
+        "attribute": Attribute,
+        "attributeGroup": AttributeGroup,
+        "choice": Choice,
+        "complexContent": ComplexContent,
+        "complexType": Complex,
+        "element": Element,
+        "enumeration": Enumeration,
+        "extension": Extension,
+        "group": Group,
+        "import": Import,
+        "include": Include,
+        "list": List,
+        "restriction": Restriction,
+        "simpleContent": SimpleContent,
+        "simpleType": Simple,
+        "sequence": Sequence,
     }
 
     @classmethod
     def maptag(cls, tag, fn):
         """
         Map (override) tag => I{class} mapping.
+
         @param tag: An XSD tag name.
         @type tag: str
         @param fn: A function or class.
         @type fn: fn|class.
+
         """
         cls.tags[tag] = fn
 
@@ -780,31 +776,35 @@ class Factory:
     def create(cls, root, schema):
         """
         Create an object based on the root tag name.
+
         @param root: An XML root element.
         @type root: L{Element}
         @param schema: A schema object.
         @type schema: L{schema.Schema}
         @return: The created object.
         @rtype: L{SchemaObject}
+
         """
         fn = cls.tags.get(root.name)
         if fn is not None:
             return fn(schema, root)
 
     @classmethod
-    def build(cls, root, schema, filter=('*',)):
+    def build(cls, root, schema, filter=("*",)):
         """
         Build an xsobject representation.
+
         @param root: An schema XML root.
         @type root: L{sax.element.Element}
         @param filter: A tag filter.
         @type filter: [str,...]
         @return: A schema object graph.
         @rtype: L{sxbase.SchemaObject}
+
         """
         children = []
         for node in root.getChildren(ns=Namespace.xsdns):
-            if '*' in filter or node.name in filter:
+            if "*" in filter or node.name in filter:
                 child = cls.create(node, schema)
                 if child is None:
                     continue
@@ -847,11 +847,11 @@ class Factory:
 # Static Import Bindings :-(
 #######################################################
 Import.bind(
-    'http://schemas.xmlsoap.org/soap/encoding/',
-    'suds://schemas.xmlsoap.org/soap/encoding/')
+    "http://schemas.xmlsoap.org/soap/encoding/",
+    "suds://schemas.xmlsoap.org/soap/encoding/")
 Import.bind(
-    'http://www.w3.org/XML/1998/namespace',
-    'http://www.w3.org/2001/xml.xsd')
+    "http://www.w3.org/XML/1998/namespace",
+    "http://www.w3.org/2001/xml.xsd")
 Import.bind(
-    'http://www.w3.org/2001/XMLSchema',
-    'http://www.w3.org/2001/XMLSchema.xsd')
+    "http://www.w3.org/2001/XMLSchema",
+    "http://www.w3.org/2001/XMLSchema.xsd")
diff --git a/suds/xsd/sxbuiltin.py b/suds/xsd/sxbuiltin.py
index 3d2c3dd..3537022 100644
--- a/suds/xsd/sxbuiltin.py
+++ b/suds/xsd/sxbuiltin.py
@@ -1,43 +1,34 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
-"""
-The I{sxbuiltin} module provides classes that represent
-XSD I{builtin} schema objects.
-"""
+"""Classes representing I{built-in} XSD schema objects."""
 
 from suds import *
 from suds.xsd import *
 from suds.sax.date import *
 from suds.xsd.sxbase import XBuiltin
 
-import datetime as dt
-
-
-class XString(XBuiltin):
-    """
-    Represents an (xsd) <xs:string/> node
-    """
-    pass
+import datetime
+import decimal
+import sys
 
 
 class XAny(XBuiltin):
-    """
-    Represents an (xsd) <any/> node
-    """
+    """Represents an XSD <xsd:any/> node."""
 
     def __init__(self, schema, name):
         XBuiltin.__init__(self, schema, name)
@@ -52,189 +43,290 @@ class XAny(XBuiltin):
 
 
 class XBoolean(XBuiltin):
-    """
-    Represents an (xsd) boolean builtin type.
-    """
+    """Represents an XSD boolean built-in type."""
 
-    translation = ({'1':True, 'true':True, '0':False, 'false':False},
-        {True:'true', 1:'true', False:'false', 0:'false'})
+    _xml_to_python = {"1": True, "true": True, "0": False, "false": False}
+    _python_to_xml = {True: "true", 1: "true", False: "false", 0: "false"}
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring):
-                return XBoolean.translation[0].get(value)
+                return XBoolean._xml_to_python.get(value)
         else:
             if isinstance(value, (bool, int)):
-                return XBoolean.translation[1].get(value)
+                return XBoolean._python_to_xml.get(value)
             return value
 
 
-class XInteger(XBuiltin):
-    """
-    Represents an (xsd) xs:int builtin type.
-    """
+class XDate(XBuiltin):
+    """Represents an XSD <xsd:date/> built-in type."""
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
-                return int(value)
+            if isinstance(value, basestring) and value:
+                return Date(value).value
         else:
-            if isinstance(value, int):
-                return str(value)
+            if isinstance(value, datetime.date):
+                return Date(value)
             return value
 
 
-class XLong(XBuiltin):
-    """
-    Represents an (xsd) xs:long builtin type.
-    """
+class XDateTime(XBuiltin):
+    """Represents an XSD <xsd:datetime/> built-in type."""
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
-                return long(value)
+            if isinstance(value, basestring) and value:
+                return DateTime(value).value
         else:
-            if isinstance(value, (int, long)):
-                return str(value)
+            if isinstance(value, datetime.datetime):
+                return DateTime(value)
             return value
 
 
-class XFloat(XBuiltin):
+class XDecimal(XBuiltin):
     """
-    Represents an (xsd) xs:float builtin type.
+    Represents an XSD <xsd:decimal/> built-in type.
+
+    Excerpt from the XSD datatype specification
+    (http://www.w3.org/TR/2004/REC-xmlschema-2-20041028):
+
+    > 3.2.3 decimal
+    >
+    > [Definition:] decimal represents a subset of the real numbers, which can
+    > be represented by decimal numerals. The ·value space· of decimal is the
+    > set of numbers that can be obtained by multiplying an integer by a
+    > non-positive power of ten, i.e., expressible as i × 10^-n where i and n
+    > are integers and n >= 0. Precision is not reflected in this value space;
+    > the number 2.0 is not distinct from the number 2.00. The ·order-relation·
+    > on decimal is the order relation on real numbers, restricted to this
+    > subset.
+    >
+    > 3.2.3.1 Lexical representation
+    >
+    > decimal has a lexical representation consisting of a finite-length
+    > sequence of decimal digits (#x30-#x39) separated by a period as a decimal
+    > indicator. An optional leading sign is allowed. If the sign is omitted,
+    > "+" is assumed. Leading and trailing zeroes are optional. If the
+    > fractional part is zero, the period and following zero(es) can be
+    > omitted. For example: -1.23, 12678967.543233, +100000.00, 210.
+
     """
 
+    # Python versions before 2.7 do not support the decimal.Decimal.canonical()
+    # method but they maintain their decimal.Decimal encoded in canonical
+    # format internally so we can easily emulate that function by simply
+    # returning the same decimal instance.
+    if sys.version_info < (2, 7):
+        _decimal_canonical = staticmethod(lambda decimal: decimal)
+    else:
+        _decimal_canonical = decimal.Decimal.canonical
+
+    @staticmethod
+    def _decimal_to_xsd_format(value):
+        """
+        Converts a decimal.Decimal value to its XSD decimal type value.
+
+        Result is a string containing the XSD decimal type's lexical value
+        representation. The conversion is done without any precision loss.
+
+        Note that Python's native decimal.Decimal string representation will
+        not do here as the lexical representation desired here does not allow
+        representing decimal values using float-like `<mantissa>E<exponent>'
+        format, e.g. 12E+30 or 0.10006E-12.
+
+        """
+        value = XDecimal._decimal_canonical(value)
+        negative, digits, exponent = value.as_tuple()
+
+        # The following implementation assumes the following tuple decimal
+        # encoding (part of the canonical decimal value encoding):
+        #  - digits must contain at least one element
+        #  - no leading integral 0 digits except a single one in 0 (if a non-0
+        #    decimal value has leading integral 0 digits they must be encoded
+        #    in its 'exponent' value and not included explicitly in its
+        #    'digits' tuple)
+        assert digits
+        assert digits[0] != 0 or len(digits) == 1
+
+        result = []
+        if negative:
+            result.append("-")
+
+        # No fractional digits.
+        if exponent >= 0:
+            result.extend(str(x) for x in digits)
+            result.extend("0" * exponent)
+            return "".join(result)
+
+        digit_count = len(digits)
+
+        # Decimal point offset from the given digit start.
+        point_offset = digit_count + exponent
+
+        # Trim trailing fractional 0 digits.
+        fractional_digit_count = min(digit_count, -exponent)
+        while fractional_digit_count and digits[digit_count - 1] == 0:
+            digit_count -= 1
+            fractional_digit_count -= 1
+
+        # No trailing fractional 0 digits and a decimal point coming not after
+        # the given digits, meaning there is no need to add additional trailing
+        # integral 0 digits.
+        if point_offset <= 0:
+            # No integral digits.
+            result.append("0")
+            if digit_count > 0:
+                result.append(".")
+                result.append("0" * -point_offset)
+                result.extend(str(x) for x in digits[:digit_count])
+        else:
+            # Have integral and possibly some fractional digits.
+            result.extend(str(x) for x in digits[:point_offset])
+            if point_offset < digit_count:
+                result.append(".")
+                result.extend(str(x) for x in digits[point_offset:digit_count])
+        return "".join(result)
+
+    @classmethod
+    def translate(cls, value, topython=True):
+        if topython:
+            if isinstance(value, basestring) and value:
+                return decimal.Decimal(value)
+        else:
+            if isinstance(value, decimal.Decimal):
+                return cls._decimal_to_xsd_format(value)
+            return value
+
+
+class XFloat(XBuiltin):
+    """Represents an XSD <xsd:float/> built-in type."""
+
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
+            if isinstance(value, basestring) and value:
                 return float(value)
         else:
-            if isinstance(value, float):
-                return str(value)
             return value
 
 
-class XDate(XBuiltin):
-    """
-    Represents an (xsd) xs:date builtin type.
-    """
+class XInteger(XBuiltin):
+    """Represents an XSD <xsd:int/> built-in type."""
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
-                return Date(value).value
+            if isinstance(value, basestring) and value:
+                return int(value)
         else:
-            if isinstance(value, dt.date):
-                return str(Date(value))
             return value
 
 
-class XTime(XBuiltin):
-    """
-    Represents an (xsd) xs:time builtin type.
-    """
+class XLong(XBuiltin):
+    """Represents an XSD <xsd:long/> built-in type."""
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
-                return Time(value).value
+            if isinstance(value, basestring) and value:
+                return long(value)
         else:
-            if isinstance(value, dt.time):
-                return str(Time(value))
             return value
 
 
-class XDateTime(XBuiltin):
-    """
-    Represents an (xsd) xs:datetime builtin type.
-    """
+class XString(XBuiltin):
+    """Represents an XSD <xsd:string/> node."""
+    pass
+
+
+class XTime(XBuiltin):
+    """Represents an XSD <xsd:time/> built-in type."""
 
     @staticmethod
     def translate(value, topython=True):
         if topython:
-            if isinstance(value, basestring) and len(value):
-                return DateTime(value).value
+            if isinstance(value, basestring) and value:
+                return Time(value).value
         else:
-            if isinstance(value, dt.datetime):
-                return str(DateTime(value))
+            if isinstance(value, datetime.time):
+                return Time(value)
             return value
 
 
 class Factory:
 
-    tags =\
-    {
+    tags = {
         # any
-        'anyType' : XAny,
+        "anyType": XAny,
         # strings
-        'string' : XString,
-        'normalizedString' : XString,
-        'ID' : XString,
-        'Name' : XString,
-        'QName' : XString,
-        'NCName' : XString,
-        'anySimpleType' : XString,
-        'anyURI' : XString,
-        'NOTATION' : XString,
-        'token' : XString,
-        'language' : XString,
-        'IDREFS' : XString,
-        'ENTITIES' : XString,
-        'IDREF' : XString,
-        'ENTITY' : XString,
-        'NMTOKEN' : XString,
-        'NMTOKENS' : XString,
+        "string": XString,
+        "normalizedString": XString,
+        "ID": XString,
+        "Name": XString,
+        "QName": XString,
+        "NCName": XString,
+        "anySimpleType": XString,
+        "anyURI": XString,
+        "NOTATION": XString,
+        "token": XString,
+        "language": XString,
+        "IDREFS": XString,
+        "ENTITIES": XString,
+        "IDREF": XString,
+        "ENTITY": XString,
+        "NMTOKEN": XString,
+        "NMTOKENS": XString,
         # binary
-        'hexBinary' : XString,
-        'base64Binary' : XString,
+        "hexBinary": XString,
+        "base64Binary": XString,
         # integers
-        'int' : XInteger,
-        'integer' : XInteger,
-        'unsignedInt' : XInteger,
-        'positiveInteger' : XInteger,
-        'negativeInteger' : XInteger,
-        'nonPositiveInteger' : XInteger,
-        'nonNegativeInteger' : XInteger,
+        "int": XInteger,
+        "integer": XInteger,
+        "unsignedInt": XInteger,
+        "positiveInteger": XInteger,
+        "negativeInteger": XInteger,
+        "nonPositiveInteger": XInteger,
+        "nonNegativeInteger": XInteger,
         # longs
-        'long' : XLong,
-        'unsignedLong' : XLong,
+        "long": XLong,
+        "unsignedLong": XLong,
         # shorts
-        'short' : XInteger,
-        'unsignedShort' : XInteger,
-        'byte' : XInteger,
-        'unsignedByte' : XInteger,
+        "short": XInteger,
+        "unsignedShort": XInteger,
+        "byte": XInteger,
+        "unsignedByte": XInteger,
         # floats
-        'float' : XFloat,
-        'double' : XFloat,
-        'decimal' : XFloat,
+        "float": XFloat,
+        "double": XFloat,
+        "decimal": XDecimal,
         # dates & times
-        'date' : XDate,
-        'time' : XTime,
-        'dateTime': XDateTime,
-        'duration': XString,
-        'gYearMonth' : XString,
-        'gYear' : XString,
-        'gMonthDay' : XString,
-        'gDay' : XString,
-        'gMonth' : XString,
+        "date": XDate,
+        "time": XTime,
+        "dateTime": XDateTime,
+        "duration": XString,
+        "gYearMonth": XString,
+        "gYear": XString,
+        "gMonthDay": XString,
+        "gDay": XString,
+        "gMonth": XString,
         # boolean
-        'boolean' : XBoolean,
+        "boolean": XBoolean,
     }
 
     @classmethod
     def maptag(cls, tag, fn):
         """
         Map (override) tag => I{class} mapping.
-        @param tag: An xsd tag name.
+
+        @param tag: An XSD tag name.
         @type tag: str
         @param fn: A function or class.
         @type fn: fn|class.
+
         """
         cls.tags[tag] = fn
 
@@ -242,14 +334,14 @@ class Factory:
     def create(cls, schema, name):
         """
         Create an object based on the root tag name.
+
         @param schema: A schema object.
         @type schema: L{schema.Schema}
         @param name: The name.
         @type name: str
         @return: The created object.
         @rtype: L{XBuiltin}
+
         """
-        fn = cls.tags.get(name)
-        if fn is not None:
-            return fn(schema, name)
-        return XBuiltin(schema, name)
+        fn = cls.tags.get(name, XBuiltin)
+        return fn(schema, name)
diff --git a/suds_jurko.egg-info/PKG-INFO b/suds_jurko.egg-info/PKG-INFO
index d4546ee..253bc0b 100644
--- a/suds_jurko.egg-info/PKG-INFO
+++ b/suds_jurko.egg-info/PKG-INFO
@@ -1,47 +1,47 @@
-Metadata-Version: 1.1
-Name: suds-jurko
-Version: 0.6
-Summary: Lightweight SOAP client (Jurko's fork)
-Home-page: http://bitbucket.org/jurko/suds
-Author: Jurko Gospodnetic
-Author-email: jurko.gospodnetic at pke.hr
-License: (specified using classifiers)
-Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.6.tar.bz2
-Description: 
-        ---------------------------------------
-        Lightweight SOAP client (Jurko's fork).
-        ---------------------------------------
-        
-          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
-        dot com) hosted at 'http://fedorahosted.org/suds'.
-        
-          'Suds' is a lightweight SOAP-based web service client for Python
-        licensed under LGPL (see the LICENSE.txt file included in the
-        distribution).
-        
-          This is hopefully just a temporary fork of the original suds Python
-        library project created because the original project development seems
-        to have stalled. Should be reintegrated back into the original project
-        if it ever gets revived again.
-        
-        
-Keywords: SOAP,web,service,client
-Platform: (specified using classifiers)
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
-Classifier: Natural Language :: English
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.4
-Classifier: Programming Language :: Python :: 2.5
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.0
-Classifier: Programming Language :: Python :: 3.1
-Classifier: Programming Language :: Python :: 3.2
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Topic :: Internet
-Obsoletes: suds
+Metadata-Version: 1.1
+Name: suds-jurko
+Version: 0.7.dev0
+Summary: Lightweight SOAP client (Jurko's fork)
+Home-page: http://bitbucket.org/jurko/suds
+Author: Jurko Gospodnetić
+Author-email: jurko.gospodnetic at pke.hr
+License: (specified using classifiers)
+Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.7.dev0.tar.bz2
+Description: 
+        ---------------------------------------
+        Lightweight SOAP client (Jurko's fork).
+        ---------------------------------------
+        
+          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
+        dot com) hosted at 'http://fedorahosted.org/suds'.
+        
+          'Suds' is a lightweight SOAP-based web service client for Python
+        licensed under LGPL (see the LICENSE.txt file included in the
+        distribution).
+        
+          This is hopefully just a temporary fork of the original suds Python
+        library project created because the original project development seems
+        to have stalled. Should be reintegrated back into the original project
+        if it ever gets revived again.
+        
+        
+Keywords: SOAP,web,service,client
+Platform: (specified using classifiers)
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.4
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.1
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Topic :: Internet
+Obsoletes: suds
diff --git a/suds_jurko.egg-info/SOURCES.txt b/suds_jurko.egg-info/SOURCES.txt
index 1f259d3..cda9f59 100644
--- a/suds_jurko.egg-info/SOURCES.txt
+++ b/suds_jurko.egg-info/SOURCES.txt
@@ -4,11 +4,14 @@ MANIFEST.in
 README.rst
 TODO.txt
 ez_setup.py
+ez_setup_1_4_2.py
 setup.cfg
 setup.py
 notes/argument_parsing.rst
 notes/readme.txt
 notes/traversing_client_data.rst
+notes/xsd_elements.rst
+notes/xsd_types.rst
 suds/__init__.py
 suds/argparser.py
 suds/builder.py
@@ -59,7 +62,7 @@ suds/umx/core.py
 suds/umx/encoded.py
 suds/umx/typed.py
 suds/xsd/__init__.py
-suds/xsd/deplist.py
+suds/xsd/depsort.py
 suds/xsd/doctor.py
 suds/xsd/query.py
 suds/xsd/schema.py
@@ -70,25 +73,54 @@ suds_jurko.egg-info/PKG-INFO
 suds_jurko.egg-info/SOURCES.txt
 suds_jurko.egg-info/dependency_links.txt
 suds_jurko.egg-info/top_level.txt
-tests/__init__.py
 tests/conftest.py
-tests/indirect_parametrize.py
 tests/test_argument_parser.py
 tests/test_cache.py
-tests/test_client_cache.py
+tests/test_client.py
+tests/test_compare_sax.py
 tests/test_date_time.py
+tests/test_dependency_sort.py
 tests/test_document_store.py
 tests/test_input_parameters.py
+tests/test_plugin.py
+tests/test_reader.py
 tests/test_reply_handling.py
 tests/test_request_construction.py
+tests/test_sax_document.py
+tests/test_sax_element.py
+tests/test_sax_encoder.py
 tests/test_suds.py
 tests/test_timezone.py
 tests/test_transport.py
 tests/test_transport_http.py
+tests/test_xsd_builtins.py
+tests/test_xsd_element.py
 tests/external/__init__.py
 tests/external/axis1.py
 tests/external/axis2.py
 tests/external/jasper.py
 tests/external/public.py
 tests/external/rhq.py
-tests/external/saxenc.py
\ No newline at end of file
+tests/external/saxenc.py
+tests/profiling/__init__.py
+tests/profiling/profile_sax_encoder.py
+tests/testutils/__init__.py
+tests/testutils/assertion.py
+tests/testutils/assertion__pytest_assert_rewrite_needed.py
+tests/testutils/compare_sax.py
+tests/testutils/compare_sax__pytest_assert_rewrite_needed.py
+tests/testutils/indirect_parametrize.py
+tools/readme.txt
+tools/run_all_tests.py
+tools/setup_base_environments.py
+tools/suds_devel/__init__.py
+tools/suds_devel/configuration.py
+tools/suds_devel/egg.py
+tools/suds_devel/environment.py
+tools/suds_devel/exception.py
+tools/suds_devel/ez_setup_versioned.py
+tools/suds_devel/parse_version.py
+tools/suds_devel/patch_pytest_on_python_31.py
+tools/suds_devel/requirements.py
+tools/suds_devel/utility.py
+tools/suds_devel/zip.py
\ No newline at end of file
diff --git a/suds_jurko.egg-info/top_level.txt b/suds_jurko.egg-info/top_level.txt
index ffb0ac3..55a2df3 100644
--- a/suds_jurko.egg-info/top_level.txt
+++ b/suds_jurko.egg-info/top_level.txt
@@ -1,2 +1 @@
-tests
 suds
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index 8212858..0000000
--- a/tests/__init__.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jurko Gospodnetić( jurko.gospodnetic at pke.hr )
-
-import suds.client
-import suds.store
-
-
-def client_from_wsdl(wsdl_content, *args, **kwargs):
-    """
-    Constructs a non-caching suds Client based on the given WSDL content.
-
-      The wsdl_content is expected to be a raw byte string and not a unicode
-    string. This simple structure suits us fine here because XML content holds
-    its own embedded encoding identification ('utf-8' if not specified
-    explicitly).
-
-      Stores the content directly inside the suds library internal document
-    store under a hard-coded id to avoid having to load the data from a
-    temporary file.
-
-      Uses a locally created empty document store unless one is provided
-    externally using the 'documentStore' keyword argument.
-
-      Explicitly disables caching or otherwise, because we use the same
-    hardcoded id for our main WSDL document, suds would always reuse the first
-    such local document from its cache instead of fetching it from our document
-    store.
-
-    """
-    assert wsdl_content.__class__ is suds.byte_str_class
-    store = kwargs.get("documentStore")
-    if store is None:
-        store = suds.store.DocumentStore()
-        kwargs.update(documentStore=store)
-    testFileId = "whatchamacallit"
-    store.update({testFileId:wsdl_content})
-    kwargs.update(cache=None)
-    return suds.client.Client("suds://" + testFileId, *args, **kwargs)
-
-
-def compare_xml(lhs, rhs):
-    """
-    Compares two XML documents.
-
-    Not intended to be perfect, but only good enough comparison to be used
-    internally inside the project's test suite.
-
-    Does not compare namespace prefixes and considers them irrelevant. This is
-    because suds may generate different namespace prefixes for the same
-    underlying XML structure when used from different Python versions.
-
-    """
-    if lhs.__class__ is not suds.sax.document.Document:
-        return False
-    if rhs.__class__ is not suds.sax.document.Document:
-        return False
-    if len(lhs.getChildren()) != 1:
-        return False
-    if len(rhs.getChildren()) != 1:
-        return False
-    return compare_xml_element(lhs.getChildren()[0], rhs.getChildren()[0])
-
-
-def compare_xml_element(lhs, rhs):
-    """
-    Compares two XML elements.
-
-    Not intended to be perfect, but only good enough comparison to be used
-    internally inside the project's test suite.
-
-    Does not compare namespace prefixes and considers them irrelevant. This is
-    because suds may generate different namespace prefixes for the same
-    underlying XML structure when used from different Python versions.
-
-    Empty string & None XML element texts are considered the same to compensate
-    for different XML object tree construction methods representing 'no text'
-    elements differently, e.g. when constructed by the sax parser or when
-    constructed in code to represent a SOAP request.
-
-    """
-    if lhs.__class__ is not suds.sax.element.Element:
-        return False
-    if rhs.__class__ is not suds.sax.element.Element:
-        return False
-    if lhs.namespace()[1] != rhs.namespace()[1]:
-        return False
-    if lhs.name != rhs.name:
-        return False
-    lhs_text = lhs.text
-    rhs_text = rhs.text
-    if lhs_text == "":
-        lhs_text = None
-    if rhs_text == "":
-        rhs_text = None
-    if lhs_text != rhs_text:
-        return False
-    if len(lhs.getChildren()) != len(rhs.getChildren()):
-        return False
-    for l, r in zip(lhs.getChildren(), rhs.getChildren()):
-        if not compare_xml_element(l, r):
-            return False
-    return True
-
-
-def compare_xml_to_string(lhs, rhs):
-    """
-    Compares two XML documents, second one given as a string.
-
-    Not intended to be perfect, but only good enough comparison to be used
-    internally inside the project's test suite.
-
-    Does not compare namespace prefixes and considers them irrelevant. This is
-    because suds may generate different namespace prefixes for the same
-    underlying XML structure when used from different Python versions.
-
-    """
-    rhs_document = suds.sax.parser.Parser().parse(string=suds.byte_str(rhs))
-    return compare_xml(lhs, rhs_document)
-
-
-def runUsingPyTest(callerGlobals):
-    """Run the caller test script using the pytest testing framework."""
-    import sys
-    # Trick setuptools into not recognizing we are referencing __file__ here.
-    # If setuptools detects __file__ usage in a module, any package containing
-    # this module will be installed as an actual folder instead of a zipped
-    # archive. This __file__ usage is safe since it is used only when a script
-    # has been run directly, and that can not be done from a zipped package
-    # archive.
-    filename = callerGlobals.get("file".join(["__"] * 2))
-    if not filename:
-        sys.exit("Internal error: can not determine test script name.")
-    try:
-        import pytest
-    except ImportError:
-        filename = filename or "<unknown-script>"
-        sys.exit("'py.test' unit testing framework not available. Can not run "
-            "'%s' directly as a script." % (filename,))
-    exitCode = pytest.main(["--pyargs", filename] + sys.argv[1:])
-    sys.exit(exitCode)
-
-
-def wsdl_input(schema_content, *args, **kwargs):
-    """
-      Returns a WSDL schema used in different suds library tests, defining a
-    single operation taking an externally specified input structure and
-    returning no output.
-
-      The operation is named 'f' by default, but a custom name may be defined
-    for it by using a 'operation_name' keyword argument.
-
-      Initial input argument is the schema part of the WSDL, any remaining
-    positional arguments are interpreted as names of included top level input
-    parameter elements.
-
-    """
-    operation_name = kwargs.pop("operation_name", "f")
-    assert not kwargs
-
-    wsdl = ["""\
-<?xml version='1.0' encoding='UTF-8'?>
-<wsdl:definitions targetNamespace="my-namespace"
-xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
-xmlns:ns="my-namespace"
-xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
-  <wsdl:types>
-    <xsd:schema targetNamespace="my-namespace"
-    elementFormDefault="qualified"
-    attributeFormDefault="unqualified"
-    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-%s
-    </xsd:schema>
-  </wsdl:types>
-  <wsdl:message name="fRequestMessage">""" % (schema_content,)]
-
-    assert len(args) >= 1
-    for arg in args:
-        wsdl.append("""\
-    <wsdl:part name="parameters" element="ns:%s" />""" % arg)
-
-    wsdl.append("""\
-  </wsdl:message>
-  <wsdl:portType name="dummyPortType">
-    <wsdl:operation name="%(name)s">
-      <wsdl:input message="ns:fRequestMessage" />
-    </wsdl:operation>
-  </wsdl:portType>
-  <wsdl:binding name="dummy" type="ns:dummyPortType">
-    <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
-    <wsdl:operation name="%(name)s">
-      <soap:operation soapAction="my-soap-action" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-    </wsdl:operation>
-  </wsdl:binding>
-  <wsdl:service name="dummy">
-    <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="unga-bunga-location" />
-    </wsdl:port>
-  </wsdl:service>
-</wsdl:definitions>
-""" % {"name":operation_name})
-
-    return suds.byte_str("\n".join(wsdl))
-
-
-def wsdl_output(schema_content, *args):
-    """
-      Returns a WSDL schema used in different suds library tests, defining a
-    single operation named f, taking no input and returning an externally
-    specified output structure.
-
-      The first input parameter is the schema part of the WSDL, the rest of the
-    parameters identify top level output parameter elements.
-
-    """
-    wsdl = ["""\
-<?xml version='1.0' encoding='UTF-8'?>
-<wsdl:definitions targetNamespace="my-namespace"
-xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
-xmlns:ns="my-namespace"
-xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
-  <wsdl:types>
-    <xsd:schema targetNamespace="my-namespace"
-    elementFormDefault="qualified"
-    attributeFormDefault="unqualified"
-    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-%s
-    </xsd:schema>
-  </wsdl:types>
-  <wsdl:message name="fResponseMessage">""" % schema_content]
-
-    assert len(args) >= 1
-    for arg in args:
-        wsdl.append("""\
-    <wsdl:part name="parameters" element="ns:%s" />""" % arg)
-
-    wsdl.append("""\
-  </wsdl:message>
-  <wsdl:portType name="dummyPortType">
-    <wsdl:operation name="f">
-      <wsdl:output message="ns:fResponseMessage" />
-    </wsdl:operation>
-  </wsdl:portType>
-  <wsdl:binding name="dummy" type="ns:dummyPortType">
-    <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
-    <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
-    </wsdl:operation>
-  </wsdl:binding>
-  <wsdl:service name="dummy">
-    <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="unga-bunga-location" />
-    </wsdl:port>
-  </wsdl:service>
-</wsdl:definitions>
-""")
-
-    return suds.byte_str("\n".join(wsdl))
diff --git a/tests/conftest.py b/tests/conftest.py
index c706614..c0a67d5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -28,4 +28,4 @@ pytest configuration file for the suds test suite.
 # folder constructed by 'setup.py build' using 'py.test build --markers'. The
 # plugin will still get loaded correctly when actually running the tests. This
 # has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+pytest_plugins = "testutils.indirect_parametrize"
diff --git a/tests/external/__init__.py b/tests/external/__init__.py
index bacf155..716dba9 100644
--- a/tests/external/__init__.py
+++ b/tests/external/__init__.py
@@ -1,17 +1,22 @@
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Test scripts inherited from the original suds library implementation, requiring
+external resources or simply untested.
+
+"""
diff --git a/tests/external/axis1.py b/tests/external/axis1.py
index 4c74c30..d316be9 100644
--- a/tests/external/axis1.py
+++ b/tests/external/axis1.py
@@ -14,20 +14,20 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 #
-# This test requires installation or visibility to my local axis(1) server.
+# This test requires installation or visibility of my local axis(1) server.
 #
 
 import sys
 sys.path.append('../../')
 
-import traceback as tb
-from tests import *
 from suds import WebFault
 from suds.client import Client
 from suds.sudsobject import Object
 from suds.transport.https import HttpAuthenticated
 from suds.plugin import *
 
+import traceback as tb
+
 
 errors = 0
 credentials = dict(username='jortel', password='abc123')
@@ -36,34 +36,34 @@ credentials = dict(username='jortel', password='abc123')
 class MyInitPlugin(InitPlugin):
 
     def initialized(self, context):
-        print 'PLUGIN (init): initialized: ctx=%s' % context.__dict__
+        print('PLUGIN (init): initialized: ctx=%s' % (context.__dict__,))
 
 
 class MyDocumentPlugin(DocumentPlugin):
 
     def loaded(self, context):
-        print 'PLUGIN (document): loaded: ctx=%s' % context.__dict__
+        print('PLUGIN (document): loaded: ctx=%s' % (context.__dict__,))
 
     def parsed(self, context):
-        print 'PLUGIN (document): parsed: ctx=%s' % context.__dict__
+        print('PLUGIN (document): parsed: ctx=%s' % (context.__dict__,))
 
 
 class MyMessagePlugin(MessagePlugin):
 
     def marshalled(self, context):
-        print 'PLUGIN (message): marshalled: ctx=%s' % context.__dict__
+        print('PLUGIN (message): marshalled: ctx=%s' % (context.__dict__,))
 
     def sending(self, context):
-        print 'PLUGIN (message): sending: ctx=%s' % context.__dict__
+        print('PLUGIN (message): sending: ctx=%s' % (context.__dict__,))
 
     def received(self, context):
-        print 'PLUGIN (message): received: ctx=%s' % context.__dict__
+        print('PLUGIN (message): received: ctx=%s' % (context.__dict__,))
 
     def parsed(self, context):
-        print 'PLUGIN (message): parsed: ctx=%s' % context.__dict__
+        print('PLUGIN (message): parsed: ctx=%s' % (context.__dict__,))
 
     def unmarshalled(self, context):
-        print 'PLUGIN: (massage): unmarshalled: ctx=%s' % context.__dict__
+        print('PLUGIN: (massage): unmarshalled: ctx=%s' % (context.__dict__,))
 
 
 myplugins = (
@@ -75,27 +75,27 @@ myplugins = (
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n'
-    print 'Test @ ( %s )\nerrors = %d\n' % (url, errors)
+    print('\n______________________________________________________________\n')
+    print('Test @ ( %s )\nerrors = %d\n' % (url, errors))
 
 try:
     url = 'http://localhost:8081/axis/services/basic-rpc-encoded?wsdl'
     start(url)
     t = HttpAuthenticated(**credentials)
     client = Client(url, transport=t, cache=None, plugins=myplugins)
-    print client
+    print(client)
     #
     # create a name object using the wsdl
     #
-    print 'create name'
+    print('create name')
     name = client.factory.create('ns0:Name')
     name.first = u'jeff'+unichr(1234)
     name.last = 'ortel'
-    print name
+    print(name)
     #
     # create a phone object using the wsdl
     #
-    print 'create phone'
+    print('create phone')
     phoneA = client.factory.create('ns0:Phone')
     phoneA.npa = 410
     phoneA.nxx = 555
@@ -119,18 +119,18 @@ try:
     # create a person object using the wsdl
     #
     person = client.factory.create('ns0:Person')
-    print '{empty} person=\n%s' % person
+    print('{empty} person=\n%s' % (person,))
     person.name = name
     person.age = 43
     person.phone = [phoneA,phoneB,phoneC]
     person.pets = [dog]
-    print 'person=\n%s' % person
+    print('person=\n%s' % (person,))
     #
     # add the person (using the webservice)
     #
-    print 'addPersion()'
+    print('addPersion()')
     result = client.service.addPerson(person)
-    print '\nreply(\n%s\n)\n' % str(result)
+    print('\nreply(\n%s\n)\n' % (str(result),))
 
     #
     # Async
@@ -159,22 +159,24 @@ try:
     ap.age = person.age
     ap.phone = person.phone
     ap.pets = person.pets
-    print 'AnotherPerson\n%s' % ap
+    print('AnotherPerson\n%s' % (ap,))
     #
     # update the person's name (using the webservice)
     #
-    print 'updatePersion()'
+    print('updatePersion()')
     result = client.service.updatePerson(ap, newname)
-    print '\nreply(\n%s\n)\n' % str(result)
+    print('\nreply(\n%s\n)\n' % (str(result),))
     result = client.service.updatePerson(ap, None)
-    print '\nreply(\n%s\n)\n' % str(result)
+    print('\nreply(\n%s\n)\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
@@ -182,19 +184,19 @@ try:
     start(url)
     t = HttpAuthenticated(**credentials)
     client = Client(url, transport=t, cache=None)
-    print client
+    print(client)
     #
     # create a name object as dict
     #
-    print 'create name'
+    print('create name')
     name = {}
     name['first'] = 'Elmer'
     name['last'] = 'Fudd'
-    print name
+    print(name)
     #
     # create a phone as dict
     #
-    print 'create phone'
+    print('create phone')
     phoneA = {}
     phoneA['npa'] = 410
     phoneA['nxx'] = 555
@@ -219,133 +221,149 @@ try:
     # create a person as dict
     #
     person = {}
-    print '{empty} person=\n%s' % person
+    print('{empty} person=\n%s' % (person,))
     person['name'] = name
     person['age'] = 43
-    person['phone'] = [phoneA,phoneB, phoneC]
+    person['phone'] = [phoneA, phoneB, phoneC]
     person['pets'] = [dog]
-    print 'person=\n%s' % person
+    print('person=\n%s' % (person,))
     #
     # add the person (using the webservice)
     #
-    print 'addPersion()'
+    print('addPersion()')
     result = client.service.addPerson(person)
-    print '\nreply(\n%s\n)\n' % str(result)
+    print('\nreply(\n%s\n)\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
-    print "echo(' this is cool ')"
+    print("echo(' this is cool ')")
     result = client.service.echo('this is cool')
-    print '\nreply( "%s" )\n' % str(result)
-    print 'echo(None)'
+    print('\nreply( "%s" )\n' % (str(result),))
+    print('echo(None)')
     result = client.service.echo(None)
-    print '\nreply( "%s" )\n' % str(result)
+    print('\nreply( "%s" )\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
-    print 'hello()'
+    print('hello()')
     result = client.service.hello()
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
-    print 'testVoid()'
+    print('testVoid()')
     result = client.service.getVoid()
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
-    print '** new style arrays **'
+    print('** new style arrays **')
     words = ['my', 'dog', 'likes', 'steak']
     result = client.service.printList(words)
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
 
-    print '** old style arrays **'
+    print('** old style arrays **')
     array = client.factory.create('ArrayOf_xsd_string')
-    print 'ArrayOf_xsd_string=\n%s' % array
+    print('ArrayOf_xsd_string=\n%s' % (array,))
     array.item = ['my', 'dog', 'likes', 'steak']
     result = client.service.printList(array)
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     s = 'hello'
     for n in range(0, 3):
-        print 'getList(%s, %d)' % (s, n)
+        print('getList(%s, %d)' % (s, n))
         result = client.service.getList(s, n)
-        print '\nreply( %s )\n' % str(result)
+        print('\nreply( %s )\n' % (str(result),))
         assert ( isinstance(result, list) and len(result) == n )
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
-    print 'testExceptions()'
+    print('testExceptions()')
     result = client.service.throwException()
-    print '\nreply( %s )\n' % tostr(result)
+    print('\nreply( %s )\n' % (tostr(result),))
     raise Exception('Fault expected and not raised')
 except WebFault, f:
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://localhost:8081/axis/services/basic-rpc-encoded?wsdl'
     start(url)
     client = Client(url, faults=False, **credentials)
-    print 'testExceptions()'
+    print('testExceptions()')
     result = client.service.throwException()
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
-print '\nFinished: errors=%d' % errors
+print('\nFinished: errors=%d' % (errors,))
diff --git a/tests/external/axis2.py b/tests/external/axis2.py
index 0a64a8f..88fb43e 100644
--- a/tests/external/axis2.py
+++ b/tests/external/axis2.py
@@ -16,15 +16,15 @@
 import sys
 sys.path.append('../../')
 
-from tests import *
 from suds import *
 from suds.client import Client
+
 from datetime import datetime
 
 
 errors = 0
 url = 'http://localhost:8080/axis2/services/BasicService?wsdl'
-print 'url=%s' % url
+print('url=%s' % (url,))
 
 #
 # create a service client using the wsdl.
@@ -34,25 +34,25 @@ client = Client(url)
 #
 # print the service (introspection)
 #
-print client
+print(client)
 
-print 'printList()'
-print client.service.printList(['a','b'])
+print('printList()')
+print(client.service.printList(['a', 'b']))
 
 #
 # create a name object using the wsdl
 #
-print 'create name'
+print('create name')
 name = client.factory.create('ns2:Name')
 name.first = u'jeff'+unichr(1234)
 name.last = 'ortel'
 
-print name
+print(name)
 
 #
 # create a phone object using the wsdl
 #
-print 'create phone'
+print('create phone')
 phoneA = client.factory.create('ns2:Phone')
 phoneA.npa = 410
 phoneA.nxx = 822
@@ -67,10 +67,10 @@ phoneB.number = 4406
 # create a dog
 #
 dog = client.factory.create('ns2:Dog')
-print dog
+print(dog)
 dog.name = 'Chance'
 dog.trained = True
-print dog
+print(dog)
 
 #
 # create a person object using the wsdl
@@ -80,7 +80,7 @@ person = client.factory.create('ns2:Person')
 #
 # inspect empty person
 #
-print '{empty} person=\n%s' % person
+print('{empty} person=\n%s' % (person,))
 
 person.name = name
 person.age = None
@@ -92,14 +92,14 @@ person.pets.append(dog)
 #
 # inspect person
 #
-print 'person=\n%s' % person
+print('person=\n%s' % (person,))
 
 #
 # add the person (using the webservice)
 #
-print 'addPersion()'
+print('addPersion()')
 result = client.service.addPerson(person)
-print '\nreply(\n%s\n)\n' % result.encode('utf-8')
+print('\nreply(\n%s\n)\n' % (result.encode('utf-8'),))
 
 #
 # create a new name object used to update the person
@@ -111,96 +111,104 @@ newname.last = None
 #
 # update the person's name (using the webservice) and print return person object
 #
-print 'updatePersion()'
+print('updatePersion()')
 result = client.service.updatePerson(person, newname)
-print '\nreply(\n%s\n)\n' % str(result)
+print('\nreply(\n%s\n)\n' % (str(result),))
 result = client.service.updatePerson(person, None)
-print '\nreply(\n%s\n)\n' % str(result)
+print('\nreply(\n%s\n)\n' % (str(result),))
 
 
 #
 # invoke the echo service
 #
-print 'echo()'
+print('echo()')
 client.service.echo(None)
 result = client.service.echo('this is cool')
-print '\nreply( %s )\n' % str(result)
+print('\nreply( %s )\n' % (str(result),))
 
-print 'echo() with {none}'
+print('echo() with {none}')
 result = client.service.echo(None)
-print '\nreply( %s )\n' % str(result)
+print('\nreply( %s )\n' % (str(result),))
 
 #
 # invoke the hello service
 #
-print 'hello()'
+print('hello()')
 result = client.service.hello()
-print '\nreply( %s )\n' % str(result)
+print('\nreply( %s )\n' % (str(result),))
 
 #
 # invoke the testVoid service
 #
 try:
-    print 'getVoid()'
+    print('getVoid()')
     result = client.service.getVoid()
-    print '\nreply( %s )\n' % str(result)
-except Exception, e:
-    print e
+    print('\nreply( %s )\n' % (str(result),))
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
+    print(sys.exc_info()[1])
 
 #
 # test list args
 #
-print 'getList(list)'
+print('getList(list)')
 mylist = ['my', 'dog', 'likes', 'steak']
 result = client.service.printList(mylist)
-print '\nreply( %s )\n' % str(result)
+print('\nreply( %s )\n' % (str(result),))
 # tuple
-print 'testListArgs(tuple)'
+print('testListArgs(tuple)')
 mylist = ('my', 'dog', 'likes', 'steak')
 result = client.service.printList(mylist)
-print '\nreply( %s )\n' % str(result)
+print('\nreply( %s )\n' % (str(result),))
 
 #
 # test list returned
 #
 for n in range(0, 3):
-    print 'getList(str, %d)' % n
+    print('getList(str, %d)' % (n,))
     result = client.service.getList('hello', n)
-    print '\nreply( %s )\n' % str(result)
+    print('\nreply( %s )\n' % (str(result),))
     assert ( isinstance(result, list) and len(result) == n )
 
-print 'addPet()'
+print('addPet()')
 dog = client.factory.create('ns2:Dog')
 dog.name = 'Chance'
 dog.trained = True
-print dog
+print(dog)
 try:
     result = client.service.addPet(person, dog)
-    print '\nreply( %s )\n' % str(result)
-except Exception, e:
-    print e
+    print('\nreply( %s )\n' % (str(result),))
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
+    print(sys.exc_info()[1])
 
-print '___________________ E X C E P T I O N S __________________________'
+print('___________________ E X C E P T I O N S __________________________')
 
 #
 # test exceptions
 #
 try:
-    print 'throwException() faults=True'
+    print('throwException() faults=True')
     result = client.service.throwException()
-    print '\nreply( %s )\n' % tostr(result)
-except Exception, e:
-    print e
+    print('\nreply( %s )\n' % (tostr(result),))
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
+    print(sys.exc_info()[1])
 
 #
 # test faults
 #
 try:
-    print 'throwException() faults=False'
+    print('throwException() faults=False')
     client.set_options(faults=False)
     result = client.service.throwException()
-    print '\nreply( %s )\n' % tostr(result)
-except Exception, e:
-    print e
+    print('\nreply( %s )\n' % (tostr(result),))
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
+    print(sys.exc_info()[1])
 
-print '\nfinished: errors=%d' % errors
+print('\nfinished: errors=%d' % (errors,))
diff --git a/tests/external/jasper.py b/tests/external/jasper.py
index fde34d5..301a6b2 100644
--- a/tests/external/jasper.py
+++ b/tests/external/jasper.py
@@ -16,32 +16,34 @@
 import sys
 sys.path.append('../../')
 
-import traceback as tb
-from tests import *
 from suds import WebFault
 from suds.client import Client
 
+import traceback as tb
+
 
 errors = 0
 
 
 def start(url):
-    print '\n________________________________________________________________\n'
-    print 'Test @ ( %s )' % url
+    print('\n______________________________________________________________\n')
+    print('Test @ ( %s )' % (url,))
 
 try:
     url = 'http://localhost:9090/jasperserver-pro/services/repository?wsdl'
     start(url)
     client = Client(url, username='jeff', password='ortel')
-    print client
-    print client.service.list('')
+    print(client)
+    print(client.service.list(''))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
-print '\nFinished: errors = %d' % errors
+print('\nFinished: errors = %d' % (errors,))
diff --git a/tests/external/public.py b/tests/external/public.py
index c1b9666..d39a0c9 100644
--- a/tests/external/public.py
+++ b/tests/external/public.py
@@ -18,7 +18,6 @@ sys.path.append('../../')
 
 import traceback as tb
 import suds.metrics as metrics
-from tests import *
 from suds import WebFault
 from suds.client import Client
 
@@ -28,231 +27,255 @@ errors = 0
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n'
-    print 'Test @ ( %s ) %d' % (url, errors)
+    print('\n______________________________________________________________\n')
+    print('Test @ ( %s ) %d' % (url, errors))
 
 try:
     url = 'http://mssoapinterop.org/asmx/simple.asmx?WSDL'
     start(url)
     client = Client(url)
-    print client
+    print(client)
     # string
     input = "42"
     d = dict(inputString=input)
     result = client.service.echoString(**d)
-    print 'echoString() = %s' % result
+    print('echoString() = %s' % (result,))
     assert result == input
     # int
     input = 42
     result = client.service.echoInteger(input)
-    print 'echoInteger() = %s' % result
+    print('echoInteger() = %s' % (result,))
     assert result == input
     # float
     input = 4.2
     result = client.service.echoFloat(input)
-    print 'echoFloat() = %s' % result
+    print('echoFloat() = %s' % (result,))
     assert result == input
     # suds 0.3.8+
     result = client.service.echoIntegerArray([])
-    print 'echoIntegerArray() = %s' % result
+    print('echoIntegerArray() = %s' % (result,))
     assert result is None
-    input = [1,2,3,4]
+    input = [1, 2, 3, 4]
     result = client.service.echoIntegerArray(input)
-    print 'echoIntegerArray() = %s' % result
+    print('echoIntegerArray() = %s' % (result,))
     assert result == input
     result = client.service.echoIntegerArray(inputIntegerArray=input)
-    print 'echoIntegerArray() = %s' % result
+    print('echoIntegerArray() = %s' % (result,))
     assert result == input
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://jira.atlassian.com/rpc/soap/jirasoapservice-v2?wsdl'
     start(url)
     client = Client(url)
-    print client
+    print(client)
     token = client.service.login('soaptester', 'soaptester')
-    print 'token="%s"' % token
+    print('token="%s"' % (token,))
     user = client.service.getUser(token, 'soaptester')
-    print 'user="%s"' % user
+    print('user="%s"' % (user,))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://jira.atlassian.com/rpc/soap/jirasoapservice-v2?wsdl'
     start(url+'  ** cloned **')
     client = Client(url).clone()
-    print '**clone**\n%s' % client
+    print('**clone**\n%s' % (client,))
     token = client.service.login('soaptester', 'soaptester')
-    print '**clone** token="%s"' % token
+    print('**clone** token="%s"' % (token,))
     user = client.service.getUser(token, 'soaptester')
-    print '**clone** user="%s"' % user
+    print('**clone** user="%s"' % (user,))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = ' http://www.boyzoid.com/comp/randomQuote.cfc?wsdl '
     start(url)
     client = Client(url)
-    print client
-    print client.service.getQuote(False)
+    print(client)
+    print(client.service.getQuote(False))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://www.zenfolio.com/zf/api/zfapi.asmx?wsdl'
     start(url)
     client = Client(url)
-    print client
+    print(client)
     #client.setport(0)
     group = client.factory.create('Group')
-    print 'Group:\n%s' % group
-    print 'LoadGroupHierarchy("demo")'
+    print('Group:\n%s' % (group,))
+    print('LoadGroupHierarchy("demo")')
     groupHierarchy = client.service.LoadGroupHierarchy("demo")
-    print 'result:\n%s' % groupHierarchy
+    print('result:\n%s' % (groupHierarchy,))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://cert.synxis.com/interface/ChannelConnect.asmx?WSDL'
     start(url)
     client = Client(url)
-    print client
+    print(client)
     #client.setport(0)
     tpa = client.factory.create('ns1:TPA_Extensions')
-    print client.service.Ping(tpa, "hello")
+    print(client.service.Ping(tpa, "hello"))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'https://sec.neurofuzz-software.com/paos/genSSHA-SOAP.php?wsdl'
     start(url)
     client = Client(url)
-    print client
-    print client.service.genSSHA('hello', 'sha1')
+    print(client)
+    print(client.service.genSSHA('hello', 'sha1'))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://ap1314-dsr.compmed.ucdavis.edu/dataserver/Aperio.Images/Image?method=wsdl'
     start(url)
     client = Client(url)
-    #print client.factory.resolver.schema
-    print client
-    print 'Logon()'
-    reply = client.service.Logon('testuser','test')
-    print reply
+    #print(client.factory.resolver.schema)
+    print(client)
+    print('Logon()')
+    reply = client.service.Logon('testuser', 'test')
+    print(reply)
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://soa.ebrev.info/service.wsdl'
     start(url)
     client = Client(url)
-    print client
+    print(client)
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = 'http://arcweb.esri.com/services/v2/MapImage.wsdl'
     start(url)
     client = Client(url)
-    print client
+    print(client)
     env = client.factory.create('ns2:Envelope')
-    print env
+    print(env)
     options = client.factory.create('ns4:MapImageOptions')
-    print options
+    print(options)
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = "http://www.thomas-bayer.com/axis2/services/BLZService?wsdl"
     start(url)
     client = Client(url)
-    print client
+    print(client)
     #client.setport(0)
-    print client.service.getBank("76251020")
+    print(client.service.getBank("76251020"))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 try:
     url = "http://arcweb.esri.com/services/v2/RouteFinder.wsdl"
     start(url)
     client = Client(url)
-    print client
+    print(client)
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
 timer = metrics.Timer()
@@ -264,19 +287,21 @@ try:
     client = Client(url)
     #client.setport(0)
     timer.stop()
-    print 'create client: %s' % timer
+    print('create client: %s' % (timer,))
     timer.start()
     s = str(client)
     timer.stop()
-    print 'str(client): %s' % timer
-    print 'client:\n%s' % s
+    print('str(client): %s' % (timer,))
+    print('client:\n%s' % (s,))
 except WebFault, f:
     errors += 1
-    print f
-    print f.fault
-except Exception, e:
+    print(f)
+    print(f.fault)
+except (KeyboardInterrupt, SystemExit):
+    raise
+except Exception:
     errors += 1
-    print e
+    print(sys.exc_info()[1])
     tb.print_exc()
 
-print '\nFinished: errors = %d' % errors
+print('\nFinished: errors = %d' % (errors,))
diff --git a/tests/external/rhq.py b/tests/external/rhq.py
index c17d578..8f2dd22 100644
--- a/tests/external/rhq.py
+++ b/tests/external/rhq.py
@@ -14,26 +14,26 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 #
-# This test requires installation or visability to an RHQ server.
+# This test requires installation or visibility of an RHQ server.
 # ( http://www.rhq-project.org )
 #
 
 import sys
 sys.path.append('../../')
 
-import traceback as tb
-from tests import *
 from suds import WebFault
 from suds.client import Client
 
+import traceback as tb
+
 
 errors = 0
 
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n'
-    print 'Test @ ( %s ) %d' % (url, errors)
+    print('\n______________________________________________________________\n')
+    print('Test @ ( %s ) %d' % (url, errors))
 
 
 def rhqTest():
@@ -43,10 +43,9 @@ def rhqTest():
     url = 'http://localhost.localdomain:7080/rhq-rhq-enterprise-server-ejb3/WebservicesManagerBean?wsdl'
     start(url)
     client = Client(url)
-    print client
+    print(client)
 
     try:
-
         #
         # create name
         #
@@ -77,22 +76,22 @@ def rhqTest():
         # create a person object using the wsdl
         #
         person = client.factory.create('person')
-        print person
+        print(person)
         person.name = name
         person.age = 43
         person.phone.append(phoneA)
         person.phone.append(phoneB)
         person.pet.append(dog)
         person.pet.append(cat)
-        print person
+        print(person)
         #
         # addPerson()
         #
-        print 'addPersion()'
+        print('addPersion()')
         result = client.service.addPerson(person)
         sent = client.last_sent()
         rcvd = client.last_received()
-        print '\nreply(\n%s\n)\n' % result
+        print('\nreply(\n%s\n)\n' % (result,))
         #
         # create a new name object used to update the person
         #
@@ -102,110 +101,124 @@ def rhqTest():
         #
         # update the person's name (using the webservice)
         #
-        print 'updatePersion()'
+        print('updatePersion()')
         result = client.service.updatePerson(person, newname)
-        print '\nreply(\n%s\n)\n' % str(result)
+        print('\nreply(\n%s\n)\n' % (str(result),))
         result = client.service.updatePerson(person, None)
-        print '\nreply(\n%s\n)\n' % str(result)
+        print('\nreply(\n%s\n)\n' % (str(result),))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
-        print "echo('this is cool')"
+        print("echo('this is cool')")
         result = client.service.echo('this is cool')
-        print '\nreply( %s )\n' % str(result)
-        print 'echo(None)'
+        print('\nreply( %s )\n' % (str(result),))
+        print('echo(None)')
         result = client.service.echo(None)
-        print '\nreply( %s )\n' % str(result)
+        print('\nreply( %s )\n' % (str(result),))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
-        print 'hello()'
+        print('hello()')
         result = client.service.hello()
-        print '\nreply( %s )\n' % str(result)
+        print('\nreply( %s )\n' % (str(result),))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
-        print 'testVoid()'
+        print('testVoid()')
         result = client.service.testVoid()
-        print '\nreply( %s )\n' % str(result)
+        print('\nreply( %s )\n' % (str(result),))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
         mylist = ['my', 'dog', 'likes', 'steak']
-        print 'testListArgs(%s)' % mylist
+        print('testListArgs(%s)' % (mylist,))
         result = client.service.testListArg(mylist)
-        print '\nreply( %s )\n' % str(result)
+        print('\nreply( %s )\n' % (str(result),))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
         s = 'hello'
         for n in range(0, 3):
-            print 'getList(%s, %d)' % (s, n)
+            print('getList(%s, %d)' % (s, n))
             result = client.service.getList(s, n)
-            print '\nreply( %s )\n' % str(result)
+            print('\nreply( %s )\n' % (str(result),))
             if len(result) != n:
                 errors += 1
-                print 'expected (%d), reply (%d)' % (n, len(result))
+                print('expected (%d), reply (%d)' % (n, len(result)))
     except WebFault, f:
         errors += 1
-        print f
-        print f.fault
-    except Exception, e:
+        print(f)
+        print(f.fault)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
     try:
-        print 'testExceptions()'
+        print('testExceptions()')
         result = client.service.testExceptions()
-        print '\nreply( %s )\n' % tostr(result)
+        print('\nreply( %s )\n' % (tostr(result),))
         raise Exception('Fault expected and not raised')
     except WebFault, f:
-        print f
-        print f.fault
-        print f.document
-    except Exception, e:
+        print(f)
+        print(f.fault)
+        print(f.document)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
         errors += 1
-        print e
+        print(sys.exc_info()[1])
         tb.print_exc()
 
 
 if __name__ == '__main__':
     errors = 0
     rhqTest()
-    print '\nFinished: errors=%d' % errors
+    print('\nFinished: errors=%d' % (errors,))
diff --git a/tests/profiling/__init__.py b/tests/profiling/__init__.py
new file mode 100644
index 0000000..37c7a06
--- /dev/null
+++ b/tests/profiling/__init__.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Package containing different suds profiling scripts used for comparing
+different suds detail implementations or running them under different Python
+interpreter versions.
+
+"""
+
+import sys
+import timeit
+
+
+class ProfilerBase(object):
+    """Base class for profilers implemented in this project."""
+
+    def __init__(self, show_each_timing=False, show_minimum=True):
+        self.show_each_timing = show_each_timing
+        self.show_minimum = show_minimum
+
+    def timeit(self, method_name, number, repeat=5):
+        print("Timing '%s' (looping %d times, %d timings)" % (method_name,
+            number, repeat))
+        # We want to pass 'self' as input to the timing functionality
+        # implemented in the timeit module, but its interface only allows
+        # sharing data via global storage.
+        timit_input_storage = "__temporary_timeit_input_data_storage__12345"
+        assert not hasattr(sys, timit_input_storage)
+        try:
+            code = "p.%s()" % (method_name,)
+            setup_code = "import sys;p = sys.%s" % (timit_input_storage,)
+            setattr(sys, timit_input_storage, self)
+            timer = timeit.Timer(code, setup=setup_code)
+            times = []
+            try:
+                for i in range(repeat):
+                    time = timer.timeit(number)
+                    times.append(time)
+                    if self.show_each_timing:
+                        print("%d. %s" % (time,))
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except Exception:
+                timer.print_exc()
+            else:
+                if self.show_minimum:
+                    print(min(times))
+            return times
+        finally:
+            try:
+                delattr(sys, timit_input_storage)
+            except AttributeError:
+                pass
diff --git a/tests/profiling/profile_sax_encoder.py b/tests/profiling/profile_sax_encoder.py
new file mode 100644
index 0000000..29b7aa9
--- /dev/null
+++ b/tests/profiling/profile_sax_encoder.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds SAX module's special character encoder profiler.
+
+"""
+
+import suds.sax.enc
+import tests.profiling
+
+import sys
+
+
+"""Profiler parameter constants."""
+NONE = 0
+SOME = 1
+MANY = 2
+QUANTITY = {NONE: "NONE", SOME: "SOME", MANY: "MANY"}
+
+
+class Profiler(tests.profiling.ProfilerBase):
+
+    def __init__(self, long_input, replacements, cdata,
+            show_each_timing=False, show_minimum=True):
+        assert long_input in (True, False)
+        assert replacements in QUANTITY
+        assert cdata in QUANTITY
+
+        super(Profiler, self).__init__(show_each_timing, show_minimum)
+
+        self.encode_input = self.__construct_input(long_input, replacements,
+            cdata, encoded=False)
+        self.decode_input = self.__construct_input(long_input, replacements,
+            cdata, encoded=True)
+
+        print("long_input=%s; replacements=%s; cdata=%s" % (long_input,
+            QUANTITY[replacements], QUANTITY[cdata]))
+        print("  encode input data length: %d" % (len(self.encode_input),))
+        print("  decode input data length: %d" % (len(self.decode_input),))
+
+    def decode(self):
+        suds.sax.enc.Encoder().decode(self.decode_input)
+
+    def encode(self):
+        suds.sax.enc.Encoder().encode(self.encode_input)
+
+    def __construct_input(self, long_input, replacements, cdata, encoded):
+        """Construct profiling input data matching the given parameters."""
+        basic_input = "All that glitters is not gold."
+        replacements_input = "<>&'"
+        if encoded:
+            replacements_input = "><&&apos"
+        cdata_input = "<![CDATA[yaba-daba-duba]]>"
+
+        # Approximate constructed input data size in bytes.
+        size = 500
+        if long_input:
+            size = 1000000
+
+        border_input = basic_input
+        if replacements != NONE:
+            border_input += replacements_input
+        if cdata != NONE:
+            border_input += cdata_input
+        if replacements == MANY:
+            basic_input += replacements_input
+        if cdata == MANY:
+            basic_input += cdata_input
+
+        border_size = 2 * len(border_input)
+        middle_size = max(size - border_size, 0)
+        middle_count = middle_size // len(basic_input) + 1
+        input = [border_input, basic_input * middle_count, border_input]
+        return "".join(input)
+
+
+if __name__ == "__main__":
+    print("Python %s" % (sys.version,))
+    print("")
+    p = Profiler(long_input=True, replacements=MANY, cdata=MANY)
+    p.timeit('encode', 18)
+    p.timeit('decode', 170)
+    print("")
+    p = Profiler(long_input=False, replacements=MANY, cdata=MANY)
+    p.timeit('encode', 24000)
+    p.timeit('decode', 140000)
+    print("")
+    p = Profiler(long_input=True, replacements=MANY, cdata=NONE)
+    p.timeit('encode', 16)
+    p.timeit('decode', 150)
+    print("")
+    p = Profiler(long_input=False, replacements=MANY, cdata=NONE)
+    p.timeit('encode', 21000)
+    p.timeit('decode', 140000)
+    print("")
+    p = Profiler(long_input=True, replacements=NONE, cdata=NONE)
+    p.timeit('encode', 250)
+    p.timeit('decode', 450)
+    print("")
+    p = Profiler(long_input=False, replacements=NONE, cdata=NONE)
+    p.timeit('encode', 240000)
+    p.timeit('decode', 200000)
+    print("")
+    p = Profiler(long_input=True, replacements=NONE, cdata=MANY)
+    p.timeit('encode', 45)
+    p.timeit('decode', 400)
+    print("")
+    p = Profiler(long_input=False, replacements=NONE, cdata=MANY)
+    p.timeit('encode', 35000)
+    p.timeit('decode', 190000)
diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py
index 6afb205..64a5778 100644
--- a/tests/test_argument_parser.py
+++ b/tests/test_argument_parser.py
@@ -27,14 +27,12 @@ specific to a particular web service operation binding.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
 import suds.argparser
-import tests
 
 import pytest
 
@@ -148,7 +146,57 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:service>
 </wsdl:definitions>
 """ % (binding_style,))
-    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    pytest.raises(MyException, client.service.f)
+    pytest.raises(MyException, client.service.f, "x")
+    pytest.raises(MyException, client.service.f, "x", "y")
+
+
+ at pytest.mark.parametrize("binding_style", (
+    "document",
+    #TODO: Suds library's RPC binding implementation should be updated to use
+    # the argument parsing functionality. This will remove code duplication
+    # between different binding implementations and make their features more
+    # balanced.
+    pytest.mark.xfail(reason="Not yet implemented.")("rpc")
+    ))
+def test_binding_for_an_operation_with_no_input_uses_argument_parsing(
+        monkeypatch, binding_style):
+    """
+    Calling web service operations should use the generic argument parsing
+    functionality independent of the operation's specific binding style.
+
+    """
+    class MyException(Exception):
+        pass
+    def raise_exception(*args, **kwargs):
+        raise MyException
+    monkeypatch.setattr(suds.argparser._ArgParser, "__init__", raise_exception)
+
+    wsdl = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f" />
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="%s" />
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""" % (binding_style,))
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
     pytest.raises(MyException, client.service.f)
     pytest.raises(MyException, client.service.f, "x")
     pytest.raises(MyException, client.service.f, "x", "y")
@@ -223,15 +271,15 @@ def test_extra_positional_arguments(param_optional, args):
 
 
 @pytest.mark.parametrize(("param_names", "args", "kwargs"), (
-    (["a"], (1,), {"a":5}),
-    ([["a"]], (1,), {"a":5}),
-    (["a"], (None, 1, 2, 7), {"a":5}),
-    ([["a"]], (None, 1, 2, 7), {"a":5}),
-    (["a", ["b"], "c"], (None, None, None), {"a":1, "b":2, "c":3}),
-    ([["a"], ["b"], ["c"]], (None, None, None), {"a":1, "b":2, "c":3}),
-    (["a"], ("x",), {"a":None}),
-    (["a", ["b"], ["c"]], (1,), {"a":None}),
-    (["a", "b", ["c"]], (None, 2), {"b":None})))
+    (["a"], (1,), {"a": 5}),
+    ([["a"]], (1,), {"a": 5}),
+    (["a"], (None, 1, 2, 7), {"a": 5}),
+    ([["a"]], (None, 1, 2, 7), {"a": 5}),
+    (["a", ["b"], "c"], (None, None, None), {"a": 1, "b": 2, "c": 3}),
+    ([["a"], ["b"], ["c"]], (None, None, None), {"a": 1, "b": 2, "c": 3}),
+    (["a"], ("x",), {"a": None}),
+    (["a", ["b"], ["c"]], (1,), {"a": None}),
+    (["a", "b", ["c"]], (None, 2), {"b": None})))
 def test_multiple_value_for_single_parameter_error(param_names, args, kwargs):
     """
     Test how multiple value for a single parameter errors are reported.
@@ -276,7 +324,7 @@ def test_not_reporting_extra_argument_errors():
         ("p2", MockParamType(True), [x, c]),
         ("p3", MockParamType(False), [x, c])]
     args = list(range(5))
-    kwargs = {"p1":"p1", "p3":"p3", "x":666}
+    kwargs = {"p1": "p1", "p3": "p3", "x": 666}
     param_processor = MockParamProcessor()
     args_required, args_allowed = suds.argparser.parse_args("w", params, args,
         kwargs, param_processor.process, False)
@@ -293,19 +341,19 @@ def test_not_reporting_extra_argument_errors():
 
 
 @pytest.mark.parametrize(("param_names", "args", "kwargs"), (
-    ([], (), {"x":5}),
-    ([], (None, 1, 2, 7), {"x":5}),
-    ([], (), {"x":1, "y":2, "z":3}),
-    (["a"], (), {"x":None}),
-    ([["a"]], (), {"x":None}),
-    (["a"], (1,), {"x":None}),
-    ([["a"]], (1,), {"x":None}),
-    (["a"], (), {"a":"spank me", "x":5}),
-    (["a"], (), {"x":5, "a":"spank me"}),
-    (["a"], (), {"a":"spank me", "x":5, "wuwu":None}),
-    (["a", "b", "c"], (1, 2), {"w":666}),
-    (["a", ["b"], ["c"]], (1,), {"c":None, "w":666}),
-    (["a", "b", ["c"]], (None,), {"b":None, "_":666})))
+    ([], (), {"x": 5}),
+    ([], (None, 1, 2, 7), {"x": 5}),
+    ([], (), {"x": 1, "y": 2, "z": 3}),
+    (["a"], (), {"x": None}),
+    ([["a"]], (), {"x": None}),
+    (["a"], (1,), {"x": None}),
+    ([["a"]], (1,), {"x": None}),
+    (["a"], (), {"a": "spank me", "x": 5}),
+    (["a"], (), {"x": 5, "a": "spank me"}),
+    (["a"], (), {"a": "spank me", "x": 5, "wuwu": None}),
+    (["a", "b", "c"], (1, 2), {"w": 666}),
+    (["a", ["b"], ["c"]], (1,), {"c": None, "w": 666}),
+    (["a", "b", ["c"]], (None,), {"b": None, "_": 666})))
 def test_unexpected_keyword_argument(param_names, args, kwargs):
     """
     Test how unexpected keyword arguments are reported.
@@ -557,4 +605,4 @@ def _expect_error(expected_exception, expected_error_text, test_function,
         else:
             assert str(e) == expected_error_text
     finally:
-        del e
+        del e  # explicitly break circular reference chain in Python 3
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 2044c96..6adbcc6 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -1,19 +1,18 @@
 # -*- coding: utf-8 -*-
 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
@@ -23,19 +22,26 @@ Implemented using the 'pytest' testing framework.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
 import suds.cache
 import suds.sax.parser
 
 import pytest
+from six import b, next, u
 
+import datetime
 import os
-import tempfile
+import os.path
+import sys
+
+
+class MyException(Exception):
+    """Local exception class used in the tests in this module."""
+    pass
 
 
 class InvisibleMan:
@@ -44,41 +50,355 @@ class InvisibleMan:
         self.x = x
 
 
+class MockDateTime(datetime.datetime):
+    """
+    MockDateTime class monkeypatched to replace datetime.datetime.
+
+    Allows us to control the exact built-in datetime.datetime.now() return
+    value. Note that Python does not allow us to monkeypatch
+    datetime.datetime.now() directly as it is a built-in function.
+
+    """
+
+    mock_counter = 0
+
+    @staticmethod
+    def now(*args, **kwargs):
+        MockDateTime.mock_counter += 1
+        return MockDateTime.mock_value
+
+
+class MockFile:
+    """
+    Wrapper around a regular file object allowing controlled file operation
+    failures.
+
+    """
+
+    def __init__(self, opener, file, fail_read):
+        self.__opener = opener
+        self.__file = file
+        self.__fail_read = fail_read
+
+    def __getattr__(self, *args, **kwargs):
+        return getattr(self.__file, *args, **kwargs)
+
+    def read(self, *args, **kwargs):
+        self.__opener.read_counter += 1
+        if self.__fail_read:
+            raise MyException
+        return self.__file.read(*args, **kwargs)
+
+
+class MockFileOpener:
+    """
+    Mock open() function for the suds.cache module.
+
+    May cause such calls to fail or to return our MockFile objects prepared so
+    some of their functions fail in a controlled manner.
+
+    """
+
+    def __init__(self, fail_open=False, fail_read=False):
+        self.__previous = None
+        self.__fail_open = fail_open
+        self.__fail_read = fail_read
+        self.counter = 0
+        self.read_counter = 0
+
+    def __call__(self, *args, **kwargs):
+        self.counter += 1
+        if self.__fail_open:
+            raise MyException
+        file = self.__previous(*args, **kwargs)
+        return MockFile(self, file, fail_read=self.__fail_read)
+
+    def apply(self, monkeypatch):
+        """Monkeypatch suds.cache module's open() global."""
+        try:
+            self.__previous = suds.cache.open
+        except AttributeError:
+            self.__previous = open
+        monkeypatch.setitem(suds.cache.__dict__, "open", self)
+
+    def reset(self):
+        self.counter = 0
+        self.read_counter = 0
+
+
+class MockParse:
+    """Mock object causing suds.sax.parser.Parser.parse() failures."""
+
+    def __init__(self):
+        self.counter = 0
+
+    def __call__(self, *args, **kwargs):
+        self.counter += 1
+        raise MyException
+
+    def apply(self, monkeypatch):
+        """Monkeypatch suds SAX Parser's parse() method."""
+        monkeypatch.setattr(suds.sax.parser.Parser, "parse", self)
+
+    def reset(self):
+        self.counter = 0
+
+
+class MockPickleLoad:
+    """Mock object causing suds.cache module's pickle load failures."""
+
+    def __init__(self):
+        self.counter = 0
+
+    def __call__(self, *args, **kwargs):
+        self.counter += 1
+        raise MyException
+
+    def apply(self, monkeypatch):
+        """Monkeypatch suds.cache module's pickle.load()."""
+        monkeypatch.setattr(suds.cache.pickle, "load", self)
+
+    def reset(self):
+        self.counter = 0
+
+
 # Hardcoded values used in different caching test cases.
-value_empty = suds.byte_str("")
-value_f2 = suds.byte_str("fifi2")
-value_f22 = suds.byte_str("fifi22")
-value_f3 = suds.byte_str("fifi3")
-value_p1 = suds.byte_str("pero1")
-value_p11 = suds.byte_str("pero11")
-value_p111 = suds.byte_str("pero111")
-value_p2 = suds.byte_str("pero2")
-value_p22 = suds.byte_str("pero22")
-value_unicode = suds.byte_str(u"€ 的 čćžšđČĆŽŠĐ")
-
-
-def test_Cache():
+value_empty = b("")
+value_f2 = b("fifi2")
+value_f22 = b("fifi22")
+value_f3 = b("fifi3")
+value_p1 = b("pero1")
+value_p11 = b("pero11")
+value_p111 = b("pero111")
+value_p2 = b("pero2")
+value_p22 = b("pero22")
+value_unicode = u("\u20AC \u7684 "
+    "\u010D\u0107\u017E\u0161\u0111"
+    "\u010C\u0106\u017D\u0160\u0110").encode("utf-8")
+
+
+# FileCache item expiration test data - duration, current_time, expect_remove.
+# Reused for different testing different FileCache derived classes.
+file_cache_item_expiration_test_data = ([
+    # Infinite cache entry durations.
+    ({}, datetime.datetime.min, False),
+    ({}, datetime.timedelta(days=-21), False),
+    ({}, -datetime.datetime.resolution, False),
+    ({}, datetime.timedelta(), False),
+    ({}, datetime.datetime.resolution, False),
+    ({}, datetime.timedelta(days=7), False),
+    ({}, datetime.datetime.max, False)] +
+    # Finite cache entry durations.
+    [(duration, current_time, expect_remove)
+        for duration in (
+            {"minutes": 7},
+            {"microseconds": 1},
+            {"microseconds": -1},
+            {"hours": -7})
+        for current_time, expect_remove in (
+            (datetime.datetime.min, False),
+            (datetime.timedelta(days=-21), False),
+            (-datetime.datetime.resolution, False),
+            (datetime.timedelta(), False),
+            (datetime.datetime.resolution, True),
+            (datetime.timedelta(days=7), True),
+            (datetime.datetime.max, True))])
+
+
+ at pytest.mark.parametrize(("method_name", "params"), (
+    ("clear", []),
+    ("get", ["id"]),
+    ("purge", ["id"]),
+    ("put", ["id", "object"])))
+def test_Cache_methods_abstract(monkeypatch, method_name, params):
+    monkeypatch.delitem(locals(), "e", False)
     cache = suds.cache.Cache()
-    pytest.raises(Exception, cache.get, "id")
-    pytest.raises(Exception, cache.put, "id", "object")
-    pytest.raises(Exception, cache.purge, "id")
-    pytest.raises(Exception, cache.clear)
-
-
-def test_DocumentCache(tmpdir):
-    cacheFolder = tmpdir.join("puffy").strpath
-    cache = suds.cache.DocumentCache(cacheFolder)
-    assert isinstance(cache, suds.cache.FileCache)
-    assert cache.get("unga1") is None
-
-    # TODO: DocumentCache class interface seems silly. Its get() operation
-    # returns an XML document while its put() operation takes an XML element.
-    # The put() operation also silently ignores passed data of incorrect type.
-    # TODO: Update this test to no longer depend on the exact input XML data
-    # formatting. We currently expect it to be formatted exactly as what gets
-    # read back from the DocumentCache.
-    content = suds.byte_str("""\
-<xsd:element name="Elemento">
+    f = getattr(cache, method_name)
+    e = pytest.raises(Exception, f, *params).value
+    try:
+        assert e.__class__ is Exception
+        assert str(e) == "not-implemented"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
+
+
+class TestDefaultFileCacheLocation:
+    """Default FileCache cache location handling tests."""
+
+    @pytest.mark.parametrize("cache_class", (
+        suds.cache.DocumentCache,
+        suds.cache.FileCache,
+        suds.cache.ObjectCache))
+    def test_basic(self, tmpdir, cache_class):
+        """
+        Test default FileCache folder usage.
+
+        Initial DocumentCache/FileCache/ObjectCache instantiation with no
+        explicitly specified location in a process should use
+        tempfile.mkdtemp() and that folder should be used as its location.
+
+        After a single DocumentCache/FileCache/ObjectCache instantiation with
+        no explicitly specified location, all later DocumentCache/FileCache/
+        ObjectCache instantiations with no explicitly specified location in the
+        same process should use that same location folder without additional
+        tempfile.mkdtemp() calls.
+
+        Both initial & non-initial DocumentCache/FileCache/ObjectCache
+        instantiation with an explicitly specified location should use that
+        folder as its default location and not make any tempfile.mkdtemp()
+        calls.
+
+        """
+        cache_folder_name = "my test cache-%s" % (cache_class.__name__,)
+        cache_folder = tmpdir.join(cache_folder_name).strpath
+        fake_cache_folder_name = "my fake cache-%s" % (cache_class.__name__,)
+        fake_cache_folder = tmpdir.join(fake_cache_folder_name).strpath
+        test_file = tmpdir.join("test_file.py")
+        test_file.write("""\
+import os.path
+
+import tempfile
+original_mkdtemp = tempfile.mkdtemp
+mock_mkdtemp_counter = 0
+def mock_mkdtemp(*args, **kwargs):
+    global mock_mkdtemp_counter
+    mock_mkdtemp_counter += 1
+    return cache_folder
+tempfile.mkdtemp = mock_mkdtemp
+
+def check_cache_folder(expected_exists, expected_mkdtemp_counter, comment):
+    if os.path.exists(cache_folder) != expected_exists:
+        if expected_exists:
+            message = "does not exist when expected"
+        else:
+            message = "exists when not expected"
+        print("Cache folder %%s (%%s)." %% (message, comment))
+        sys.exit(-2)
+    if mock_mkdtemp_counter != expected_mkdtemp_counter:
+        if mock_mkdtemp_counter < expected_mkdtemp_counter:
+            message = "less"
+        else:
+            message = "more"
+        print("tempfile.mkdtemp() called %%s times then expected (%%s)" %%
+            (message, comment,))
+
+cache_folder = %(cache_folder)r
+fake_cache_folder = %(fake_cache_folder)r
+def fake_cache(n):
+    return fake_cache_folder + str(n)
+
+from suds.cache import DocumentCache, FileCache, ObjectCache
+check_cache_folder(False, 0, "import")
+
+assert DocumentCache(fake_cache(1)).location == fake_cache(1)
+assert FileCache(fake_cache(2)).location == fake_cache(2)
+assert ObjectCache(fake_cache(3)).location == fake_cache(3)
+check_cache_folder(False, 0, "initial caches with non-default location")
+
+assert %(cache_class_name)s().location == cache_folder
+check_cache_folder(True, 1, "initial cache with default location")
+
+assert DocumentCache().location == cache_folder
+assert FileCache().location == cache_folder
+assert ObjectCache().location == cache_folder
+check_cache_folder(True, 1, "non-initial caches with default location")
+
+assert DocumentCache(fake_cache(4)).location == fake_cache(4)
+assert FileCache(fake_cache(5)).location == fake_cache(5)
+assert ObjectCache(fake_cache(6)).location == fake_cache(6)
+check_cache_folder(True, 1, "non-initial caches with non-default location")
+
+assert DocumentCache().location == cache_folder
+assert FileCache().location == cache_folder
+assert ObjectCache().location == cache_folder
+check_cache_folder(True, 1, "final caches with default location")
+""" % {"cache_class_name": cache_class.__name__,
+    "cache_folder": cache_folder,
+    "fake_cache_folder": fake_cache_folder})
+
+        assert not os.path.exists(cache_folder)
+        testutils.run_test_process(test_file)
+
+    @pytest.mark.parametrize("removal_enabled", (True, False))
+    def test_remove_on_exit(self, tmpdir, removal_enabled):
+        """
+        Test removing the default cache folder on process exit.
+
+        The folder should be removed by default on process exit, but this
+        behaviour may be disabled by the user.
+
+        """
+        cache_folder_name = "my test cache-%s" % (removal_enabled,)
+        cache_folder = tmpdir.join(cache_folder_name).strpath
+        test_file = tmpdir.join("test_file.py")
+        test_file.write("""\
+import os.path
+
+import tempfile
+original_mkdtemp = tempfile.mkdtemp
+mock_mkdtemp_counter = 0
+def mock_mkdtemp(*args, **kwargs):
+    global mock_mkdtemp_counter
+    mock_mkdtemp_counter += 1
+    return cache_folder
+tempfile.mkdtemp = mock_mkdtemp
+
+import suds.cache
+if not suds.cache.FileCache.remove_default_location_on_exit:
+    print("Default FileCache folder removal not enabled by default.")
+    sys.exit(-2)
+suds.cache.FileCache.remove_default_location_on_exit = %(removal_enabled)s
+
+cache_folder = %(cache_folder)r
+if os.path.exists(cache_folder):
+    print("Cache folder exists too early.")
+    sys.exit(-2)
+
+suds.cache.FileCache()
+
+if not mock_mkdtemp_counter == 1:
+    print("tempfile.mkdtemp() not called as expected (%%d)." %%
+        (mock_mkdtemp_counter,))
+    sys.exit(-2)
+
+if not os.path.isdir(cache_folder):
+    print("Cache folder not created when expected.")
+    sys.exit(-2)
+""" % {"cache_folder": cache_folder, "removal_enabled": removal_enabled})
+
+        assert not os.path.exists(cache_folder)
+        testutils.run_test_process(test_file)
+        if removal_enabled:
+            assert not os.path.exists(cache_folder)
+        else:
+            assert os.path.isdir(cache_folder)
+
+
+class TestDocumentCache:
+
+    def compare_document_to_content(self, document, content):
+        """Assert that the given XML document and content match."""
+        assert document.__class__ is suds.sax.document.Document
+        elements = document.getChildren()
+        assert len(elements) == 1
+        element = elements[0]
+        assert element.__class__ is suds.sax.element.Element
+        assert suds.byte_str(str(element)) == content
+
+    @staticmethod
+    def construct_XML(element_name="Elemento"):
+        """
+        Construct XML content and a Document wrapping it.
+
+        The XML contains a single Element (may be parametrized with the given
+        element name) and possibly additional sub-elements under it.
+
+        """
+        #TODO: Update the tests in this group to no longer depend on the exact
+        # input XML data formatting. They currently expect it to be formatted
+        # exactly as what gets read back from their DocumentCache.
+        content = suds.byte_str("""\
+<xsd:element name="%s">
    <xsd:simpleType>
       <xsd:restriction base="xsd:string">
          <xsd:enumeration value="alfa"/>
@@ -86,232 +406,488 @@ def test_DocumentCache(tmpdir):
          <xsd:enumeration value="gamma"/>
       </xsd:restriction>
    </xsd:simpleType>
-</xsd:element>""")
-    xml = suds.sax.parser.Parser().parse(suds.BytesIO(content))
-    cache.put("unga1", xml.getChildren()[0])
-    readXML = cache.get("unga1")
-    assert isinstance(readXML, suds.sax.document.Document)
-    readXMLElements = readXML.getChildren()
-    assert len(readXMLElements) == 1
-    readXMLElement = readXMLElements[0]
-    assert isinstance(readXMLElement, suds.sax.element.Element)
-    assert suds.byte_str(str(readXMLElement)) == content
-
-
-def test_FileCache():
-    cache = suds.cache.FileCache()
-    assert isinstance(cache, suds.cache.Cache)
+</xsd:element>""" % (element_name,))
+        xml = suds.sax.parser.Parser().parse(suds.BytesIO(content))
+        assert xml.__class__ is suds.sax.document.Document
+        return content, xml
+
+    def test_cache_document(self, tmpdir):
+        cache_item_id = "unga1"
+        cache = suds.cache.DocumentCache(tmpdir.strpath)
+        assert isinstance(cache, suds.cache.FileCache)
+        assert cache.get(cache_item_id) is None
+        content, document = self.construct_XML()
+        cache.put(cache_item_id, document)
+        self.compare_document_to_content(cache.get(cache_item_id), content)
+
+    def test_cache_element(self, tmpdir):
+        cache_item_id = "unga1"
+        cache = suds.cache.DocumentCache(tmpdir.strpath)
+        assert isinstance(cache, suds.cache.FileCache)
+        assert cache.get(cache_item_id) is None
+        content, document = self.construct_XML()
+        cache.put(cache_item_id, document.root())
+        self.compare_document_to_content(cache.get(cache_item_id), content)
+
+    def test_file_open_failure(self, tmpdir, monkeypatch):
+        """
+        File open failure should cause no cached object to be found, but any
+        existing underlying cache file should be kept around.
 
+        """
+        mock_open = MockFileOpener(fail_open=True)
 
-def test_FileCache_clear(tmpdir):
-    cacheFolder1 = tmpdir.join("fungus").strpath
-    cache1 = suds.cache.FileCache(cacheFolder1)
-    cache1.put("unga1", value_p1)
-    cache1.put("unga2", value_p2)
-    assert cache1.get("unga1") == value_p1
-    assert cache1.get("unga2") == value_p2
-    cache1.clear()
-    assert _isEmptyCacheFolder(cacheFolder1)
-    assert cache1.get("unga1") is None
-    assert cache1.get("unga2") is None
-    cache1.put("unga1", value_p11)
-    cache1.put("unga2", value_p2)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-
-    cacheFolder2 = tmpdir.join("broccoli").strpath
-    cache2 = suds.cache.FileCache(cacheFolder2)
-    cache2.put("unga2", value_f2)
-    assert cache2.get("unga2") == value_f2
-    cache2.clear()
-    assert not _isEmptyCacheFolder(cacheFolder1)
-    assert _isEmptyCacheFolder(cacheFolder2)
-    assert cache2.get("unga2") is None
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-    cache2.put("unga2", value_p22)
-    assert cache2.get("unga2") == value_p22
-
-
-def test_FileCache_location(tmpdir):
-    defaultLocation = os.path.join(tempfile.gettempdir(), "suds")
-    cache = suds.cache.FileCache()
-    assert os.path.isdir(cache.location)
-    assert cache.location == defaultLocation
-    assert suds.cache.FileCache().location == defaultLocation
-    assert cache.location == defaultLocation
-
-    cacheFolder1 = tmpdir.join("flip-flop1").strpath
-    assert not os.path.isdir(cacheFolder1)
-    assert suds.cache.FileCache(location=cacheFolder1).location == cacheFolder1
-    assert _isEmptyCacheFolder(cacheFolder1)
-
-    cacheFolder2 = tmpdir.join("flip-flop2").strpath
-    assert not os.path.isdir(cacheFolder2)
-    assert suds.cache.FileCache(cacheFolder2).location == cacheFolder2
-    assert _isEmptyCacheFolder(cacheFolder2)
-
-
-def test_FileCache_close_leaves_cached_files_behind(tmpdir):
-    cacheFolder1 = tmpdir.join("ana").strpath
-    cache1 = suds.cache.FileCache(cacheFolder1)
-    cache1.put("unga1", value_p1)
-    cache1.put("unga2", value_p2)
-
-    cacheFolder2 = tmpdir.join("nan").strpath
-    cache2 = suds.cache.FileCache(cacheFolder2)
-    cache2.put("unga2", value_f2)
-    cache2.put("unga3", value_f3)
-
-    del cache1
-
-    cache11 = suds.cache.FileCache(cacheFolder1)
-    assert cache11.get("unga1") == value_p1
-    assert cache11.get("unga2") == value_p2
-    assert cache2.get("unga2") == value_f2
-    assert cache2.get("unga3") == value_f3
-
-
-def test_FileCache_get_put(tmpdir):
-    cacheFolder1 = tmpdir.join("firefly").strpath
-    cache1 = suds.cache.FileCache(cacheFolder1)
-    assert _isEmptyCacheFolder(cacheFolder1)
-    assert cache1.get("unga1") is None
-    cache1.put("unga1", value_p1)
-    assert not _isEmptyCacheFolder(cacheFolder1)
-    assert cache1.get("unga1") == value_p1
-    assert cache1.get("unga2") is None
-    cache1.put("unga1", value_p11)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") is None
-    cache1.put("unga2", value_p2)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-
-    cacheFolder2 = tmpdir.join("semper fi").strpath
-    cache2 = suds.cache.FileCache(cacheFolder2)
-    assert _isEmptyCacheFolder(cacheFolder2)
-    assert cache2.get("unga2") is None
-    cache2.put("unga2", value_f2)
-    assert not _isEmptyCacheFolder(cacheFolder2)
-    assert cache2.get("unga2") == value_f2
-    assert cache2.get("unga3") is None
-    cache2.put("unga2", value_f22)
-    assert cache2.get("unga2") == value_f22
-    assert cache2.get("unga3") is None
-    cache2.put("unga3", value_f3)
-    assert cache2.get("unga2") == value_f22
-    assert cache2.get("unga3") == value_f3
-
-    assert not _isEmptyCacheFolder(cacheFolder1)
-    assert not _isEmptyCacheFolder(cacheFolder2)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-    assert cache1.get("unga3") is None
-    assert cache2.get("unga1") is None
-    assert cache2.get("unga2") == value_f22
-    assert cache2.get("unga3") == value_f3
-
-
-def test_FileCache_purge(tmpdir):
-    cacheFolder1 = tmpdir.join("flamenco").strpath
-    cache1 = suds.cache.FileCache(cacheFolder1)
-    cache1.put("unga1", value_p1)
-    assert cache1.get("unga1") == value_p1
-    cache1.purge("unga1")
-    assert _isEmptyCacheFolder(cacheFolder1)
-    assert cache1.get("unga1") is None
-    cache1.put("unga1", value_p11)
-    cache1.put("unga2", value_p2)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-    cache1.purge("unga1")
-    assert cache1.get("unga1") is None
-    assert cache1.get("unga2") == value_p2
-    cache1.put("unga1", value_p111)
-
-    cacheFolder2 = tmpdir.join("shadow").strpath
-    cache2 = suds.cache.FileCache(cacheFolder2)
-    cache2.put("unga2", value_f2)
-    cache2.purge("unga2")
-    assert _isEmptyCacheFolder(cacheFolder2)
-    assert cache1.get("unga1") == value_p111
-    assert cache1.get("unga2") == value_p2
-    assert cache2.get("unga2") is None
-
-
-def test_FileCache_reused_cache_folder(tmpdir):
-    cacheFolder = tmpdir.strpath
-    cache1 = suds.cache.FileCache(cacheFolder)
-    assert _isEmptyCacheFolder(cacheFolder)
-    assert cache1.get("unga1") is None
-    cache1.put("unga1", value_p1)
-    assert cache1.get("unga1") == value_p1
-    assert cache1.get("unga2") is None
-    cache1.put("unga1", value_p11)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") is None
-    cache1.put("unga2", value_p2)
-    assert cache1.get("unga1") == value_p11
-    assert cache1.get("unga2") == value_p2
-
-    cache2 = suds.cache.FileCache(cacheFolder)
-    assert cache2.get("unga1") == value_p11
-    assert cache2.get("unga2") == value_p2
-    cache2.put("unga3", value_f3)
-    assert cache1.get("unga3") == value_f3
-
-
-def test_FileCache_version(tmpdir):
-    fakeVersionInfo = "--- fake version info ---"
-    assert suds.__version__ != fakeVersionInfo
-
-    cacheFolder = tmpdir.join("hitori")
-    versionFile = cacheFolder.join("version")
-    cache = suds.cache.FileCache(cacheFolder.strpath)
-    assert versionFile.read() == suds.__version__
-    cache.put("unga1", value_p1)
-
-    versionFile.write(fakeVersionInfo)
-    assert cache.get("unga1") == value_p1
-
-    cache2 = suds.cache.FileCache(cacheFolder.strpath)
-    assert _isEmptyCacheFolder(cacheFolder.strpath)
-    assert cache.get("unga1") is None
-    assert cache2.get("unga1") is None
-    assert versionFile.read() == suds.__version__
-    cache.put("unga1", value_p11)
-    cache.put("unga2", value_p22)
-
-    versionFile.remove()
-    assert cache.get("unga1") == value_p11
-    assert cache.get("unga2") == value_p22
-
-    cache3 = suds.cache.FileCache(cacheFolder.strpath)
-    assert _isEmptyCacheFolder(cacheFolder.strpath)
-    assert cache.get("unga1") is None
-    assert cache.get("unga2") is None
-    assert cache2.get("unga1") is None
-    assert versionFile.read() == suds.__version__
-
-
-def test_FileCache_with_empty_cached_content(tmpdir):
-    cacheFolder = tmpdir.strpath
-    cache = suds.cache.FileCache(cacheFolder)
-    cache.put("unga1", value_empty)
-    assert cache.get("unga1") == value_empty
-    assert not _isEmptyCacheFolder(cacheFolder)
-
-
-def test_FileCache_with_random_utf_character_cached_content(tmpdir):
-    cacheFolder = tmpdir.strpath
-    cache = suds.cache.FileCache(cacheFolder)
-    cache.put("unga1", value_unicode)
-    assert cache.get("unga1") == value_unicode
-    assert not _isEmptyCacheFolder(cacheFolder)
-
-
-def test_NoCache():
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.DocumentCache(cache_folder)
+        content1, document1 = self.construct_XML("One")
+        content2, document2 = self.construct_XML("Two")
+        assert content1 != content2
+        cache.put("unga1", document1)
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 1
+        _assert_empty_cache_folder(cache_folder, expected=False)
+        self.compare_document_to_content(cache.get("unga1"), content1)
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga2") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 2
+        _assert_empty_cache_folder(cache_folder, expected=False)
+        self.compare_document_to_content(cache.get("unga1"), content1)
+        assert cache.get("unga2") is None
+
+        cache.put("unga2", document2)
+        assert mock_open.counter == 2
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 3
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+        self.compare_document_to_content(cache.get("unga1"), content1)
+        self.compare_document_to_content(cache.get("unga2"), content2)
+        assert mock_open.counter == 3
+
+    @pytest.mark.parametrize(("mock", "extra_checks"), (
+        (MockParse(), [lambda x: True] * 4),
+        (MockFileOpener(fail_read=True), [
+            lambda x: x.read_counter != 0,
+            lambda x: x.read_counter == 0,
+            lambda x: x.read_counter != 0,
+            lambda x: x.read_counter == 0])))
+    def test_file_operation_failure(self, tmpdir, monkeypatch, mock,
+            extra_checks):
+        """
+        File operation failures such as reading failures or failing to parse
+        data read from such a file should cause no cached object to be found
+        and the related cache file to be removed.
+
+        """
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.DocumentCache(cache_folder)
+        content1, document1 = self.construct_XML("Eins")
+        content2, document2 = self.construct_XML("Zwei")
+        cache.put("unga1", document1)
+
+        mock.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock.counter == 1
+        assert extra_checks[0](mock)
+        _assert_empty_cache_folder(cache_folder)
+
+        mock.reset()
+        assert cache.get("unga1") is None
+        cache.put("unga1", document1)
+        cache.put("unga2", document2)
+        assert mock.counter == 0
+        assert extra_checks[1](mock)
+
+        mock.reset()
+        mock.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock.counter == 1
+        assert extra_checks[2](mock)
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+        mock.reset()
+        assert cache.get("unga1") is None
+        self.compare_document_to_content(cache.get("unga2"), content2)
+        assert mock.counter == 0
+        assert extra_checks[3](mock)
+
+    @pytest.mark.parametrize(("duration", "current_time", "expect_remove"),
+        file_cache_item_expiration_test_data)
+    def test_item_expiration(self, tmpdir, monkeypatch, duration, current_time,
+            expect_remove):
+        """See TestFileCache.item_expiration_test_worker() for more info."""
+        cache = suds.cache.DocumentCache(tmpdir.strpath, **duration)
+        content, document = self.construct_XML()
+        cache.put("willy", document)
+        TestFileCache.item_expiration_test_worker(cache, "willy", monkeypatch,
+            current_time, expect_remove)
+
+    def test_repeated_reads(self, tmpdir):
+        cache = suds.cache.DocumentCache(tmpdir.strpath)
+        content, document = self.construct_XML()
+        cache.put("unga1", document)
+        read_XML = cache.get("unga1").str()
+        assert read_XML == cache.get("unga1").str()
+        assert cache.get(None) is None
+        assert cache.get("") is None
+        assert cache.get("unga2") is None
+        assert read_XML == cache.get("unga1").str()
+
+
+class TestFileCache:
+
+    @staticmethod
+    def item_expiration_test_worker(cache, id, monkeypatch, current_time,
+            expect_remove):
+        """
+        Test how a FileCache & its derived classes expire their item entries.
+
+        Facts tested:
+        * 0 duration should cause cache items never to expire.
+        * Expired item files should be automatically removed from the cache
+          folder.
+        * Negative durations should be treated the same as positive ones.
+
+        Requirements on the passed cache object:
+        * Configures with the correct duration for this test.
+        * Contains a valid cached item with the given id and its ctime
+          timestamp + cache.duration must fall into the valid datetime.datetime
+          value range.
+        * Must use only public & protected FileCache interfaces to access its
+          cache item data files.
+
+        'current_time' values are expected to be either datetime.datetime or
+        datetime.timedelta instances with the latter interpreted relative to
+        the test file's expected expiration time.
+
+        """
+        assert isinstance(cache, suds.cache.FileCache)
+        filepath = cache._FileCache__filename(id)
+        assert os.path.isfile(filepath)
+        file_timestamp = os.path.getctime(filepath)
+        file_time = datetime.datetime.fromtimestamp(file_timestamp)
+
+        MockDateTime.mock_counter = 0
+        if isinstance(current_time, datetime.timedelta):
+            expire_time = file_time + cache.duration
+            MockDateTime.mock_value = expire_time + current_time
+        else:
+            MockDateTime.mock_value = current_time
+        monkeypatch.setattr(datetime, "datetime", MockDateTime)
+        assert (cache._getf(id) is None) == expect_remove
+        monkeypatch.undo()
+        if cache.duration:
+            assert MockDateTime.mock_counter == 1
+        else:
+            assert MockDateTime.mock_counter == 0
+        assert os.path.isfile(filepath) == (not expect_remove)
+
+    def test_basic_construction(self):
+        cache = suds.cache.FileCache()
+        assert isinstance(cache, suds.cache.Cache)
+        assert cache.duration.__class__ is datetime.timedelta
+
+    def test_cached_content_empty(self, tmpdir):
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.FileCache(cache_folder)
+        cache.put("unga1", value_empty)
+        assert cache.get("unga1") == value_empty
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+    def test_cached_content_unicode(self, tmpdir):
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.FileCache(cache_folder)
+        cache.put("unga1", value_unicode)
+        assert cache.get("unga1") == value_unicode
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+    def test_clear(self, tmpdir):
+        cache_folder1 = tmpdir.join("fungus").strpath
+        cache1 = suds.cache.FileCache(cache_folder1)
+        cache1.put("unga1", value_p1)
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        cache1.put("unga2", value_p2)
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        assert cache1.get("unga1") == value_p1
+        assert cache1.get("unga2") == value_p2
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        cache1.clear()
+        _assert_empty_cache_folder(cache_folder1)
+        assert cache1.get("unga1") is None
+        assert cache1.get("unga2") is None
+        _assert_empty_cache_folder(cache_folder1)
+        cache1.put("unga1", value_p11)
+        cache1.put("unga2", value_p2)
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+
+        cache_folder2 = tmpdir.join("broccoli").strpath
+        cache2 = suds.cache.FileCache(cache_folder2)
+        cache2.put("unga2", value_f2)
+        assert cache2.get("unga2") == value_f2
+        assert cache1.get("unga2") == value_p2
+        cache2.clear()
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        _assert_empty_cache_folder(cache_folder2)
+        assert cache2.get("unga2") is None
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+        cache2.put("unga2", value_p22)
+        assert cache2.get("unga2") == value_p22
+
+    def test_close_leaves_cached_files_behind(self, tmpdir):
+        cache_folder1 = tmpdir.join("ana").strpath
+        cache1 = suds.cache.FileCache(cache_folder1)
+        cache1.put("unga1", value_p1)
+        cache1.put("unga2", value_p2)
+
+        cache_folder2 = tmpdir.join("nan").strpath
+        cache2 = suds.cache.FileCache(cache_folder2)
+        cache2.put("unga2", value_f2)
+        cache2.put("unga3", value_f3)
+
+        del cache1
+
+        cache11 = suds.cache.FileCache(cache_folder1)
+        assert cache11.get("unga1") == value_p1
+        assert cache11.get("unga2") == value_p2
+        assert cache2.get("unga2") == value_f2
+        assert cache2.get("unga3") == value_f3
+
+    def test_get_put(self, tmpdir):
+        cache_folder1 = tmpdir.join("firefly").strpath
+        cache1 = suds.cache.FileCache(cache_folder1)
+        _assert_empty_cache_folder(cache_folder1)
+        assert cache1.get("unga1") is None
+        cache1.put("unga1", value_p1)
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        assert cache1.get("unga1") == value_p1
+        assert cache1.get("unga2") is None
+        cache1.put("unga1", value_p11)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") is None
+        cache1.put("unga2", value_p2)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+
+        cache_folder2 = tmpdir.join("semper fi").strpath
+        cache2 = suds.cache.FileCache(cache_folder2)
+        _assert_empty_cache_folder(cache_folder2)
+        assert cache2.get("unga2") is None
+        cache2.put("unga2", value_f2)
+        _assert_empty_cache_folder(cache_folder2, expected=False)
+        assert cache2.get("unga2") == value_f2
+        assert cache2.get("unga3") is None
+        cache2.put("unga2", value_f22)
+        assert cache2.get("unga2") == value_f22
+        assert cache2.get("unga3") is None
+        cache2.put("unga3", value_f3)
+        assert cache2.get("unga2") == value_f22
+        assert cache2.get("unga3") == value_f3
+
+        _assert_empty_cache_folder(cache_folder1, expected=False)
+        _assert_empty_cache_folder(cache_folder2, expected=False)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+        assert cache1.get("unga3") is None
+        assert cache2.get("unga1") is None
+        assert cache2.get("unga2") == value_f22
+        assert cache2.get("unga3") == value_f3
+
+    def test_independent_item_expirations(self, tmpdir, monkeypatch):
+        cache = suds.cache.FileCache(tmpdir.strpath, days=1)
+        cache.put("unga1", value_p1)
+        cache.put("unga2", value_p2)
+        cache.put("unga3", value_f2)
+        filepath1 = cache._FileCache__filename("unga1")
+        filepath2 = cache._FileCache__filename("unga2")
+        filepath3 = cache._FileCache__filename("unga3")
+        file_timestamp1 = os.path.getctime(filepath1)
+        file_timestamp2 = file_timestamp1 + 10 * 60  # in seconds
+        file_timestamp3 = file_timestamp1 + 20 * 60  # in seconds
+        file_time1 = datetime.datetime.fromtimestamp(file_timestamp1)
+        file_time1_expiration = file_time1 + cache.duration
+
+        original_getctime = os.path.getctime
+        def mock_getctime(path):
+            if path == filepath2:
+                return file_timestamp2
+            if path == filepath3:
+                return file_timestamp3
+            return original_getctime(path)
+
+        timedelta = datetime.timedelta
+
+        monkeypatch.setattr(os.path, "getctime", mock_getctime)
+        monkeypatch.setattr(datetime, "datetime", MockDateTime)
+
+        MockDateTime.mock_value = file_time1_expiration + timedelta(minutes=15)
+        assert cache._getf("unga2") is None
+        assert os.path.isfile(filepath1)
+        assert not os.path.isfile(filepath2)
+        assert os.path.isfile(filepath3)
+
+        cache._getf("unga3").close()
+        assert os.path.isfile(filepath1)
+        assert not os.path.isfile(filepath2)
+        assert os.path.isfile(filepath3)
+
+        MockDateTime.mock_value = file_time1_expiration + timedelta(minutes=25)
+        assert cache._getf("unga1") is None
+        assert not os.path.isfile(filepath1)
+        assert not os.path.isfile(filepath2)
+        assert os.path.isfile(filepath3)
+
+        assert cache._getf("unga3") is None
+        assert not os.path.isfile(filepath1)
+        assert not os.path.isfile(filepath2)
+        assert not os.path.isfile(filepath3)
+
+    @pytest.mark.parametrize(("duration", "current_time", "expect_remove"),
+        file_cache_item_expiration_test_data)
+    def test_item_expiration(self, tmpdir, monkeypatch, duration, current_time,
+            expect_remove):
+        """See TestFileCache.item_expiration_test_worker() for more info."""
+        cache = suds.cache.FileCache(tmpdir.strpath, **duration)
+        cache.put("unga1", value_p1)
+        TestFileCache.item_expiration_test_worker(cache, "unga1", monkeypatch,
+            current_time, expect_remove)
+
+    def test_non_default_location(self, tmpdir):
+        FileCache = suds.cache.FileCache
+
+        cache_folder1 = tmpdir.join("flip-flop1").strpath
+        assert not os.path.isdir(cache_folder1)
+        assert FileCache(location=cache_folder1).location == cache_folder1
+        _assert_empty_cache_folder(cache_folder1)
+
+        cache_folder2 = tmpdir.join("flip-flop2").strpath
+        assert not os.path.isdir(cache_folder2)
+        assert FileCache(cache_folder2).location == cache_folder2
+        _assert_empty_cache_folder(cache_folder2)
+
+    def test_purge(self, tmpdir):
+        cache_folder1 = tmpdir.join("flamenco").strpath
+        cache1 = suds.cache.FileCache(cache_folder1)
+        cache1.put("unga1", value_p1)
+        assert cache1.get("unga1") == value_p1
+        cache1.purge("unga1")
+        _assert_empty_cache_folder(cache_folder1)
+        assert cache1.get("unga1") is None
+        cache1.put("unga1", value_p11)
+        cache1.put("unga2", value_p2)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+        cache1.purge("unga1")
+        assert cache1.get("unga1") is None
+        assert cache1.get("unga2") == value_p2
+        cache1.put("unga1", value_p111)
+
+        cache_folder2 = tmpdir.join("shadow").strpath
+        cache2 = suds.cache.FileCache(cache_folder2)
+        cache2.put("unga2", value_f2)
+        cache2.purge("unga2")
+        _assert_empty_cache_folder(cache_folder2)
+        assert cache1.get("unga1") == value_p111
+        assert cache1.get("unga2") == value_p2
+        assert cache2.get("unga2") is None
+
+    def test_reused_cache_folder(self, tmpdir):
+        cache_folder = tmpdir.strpath
+        cache1 = suds.cache.FileCache(cache_folder)
+        _assert_empty_cache_folder(cache_folder)
+        assert cache1.get("unga1") is None
+        cache1.put("unga1", value_p1)
+        assert cache1.get("unga1") == value_p1
+        assert cache1.get("unga2") is None
+        cache1.put("unga1", value_p11)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") is None
+        cache1.put("unga2", value_p2)
+        assert cache1.get("unga1") == value_p11
+        assert cache1.get("unga2") == value_p2
+
+        cache2 = suds.cache.FileCache(cache_folder)
+        assert cache2.get("unga1") == value_p11
+        assert cache2.get("unga2") == value_p2
+        cache2.put("unga2", value_f2)
+        cache2.put("unga3", value_f3)
+        assert cache1.get("unga2") == value_f2
+        assert cache1.get("unga3") == value_f3
+        cache1.purge("unga2")
+        assert cache2.get("unga2") is None
+        cache1.clear()
+        assert cache2.get("unga1") is None
+        assert cache2.get("unga3") is None
+
+    @pytest.mark.parametrize("params", (
+        {},
+        {"microseconds": 1},
+        {"milliseconds": 1},
+        {"seconds": 1},
+        {"minutes": 1},
+        {"hours": 1},
+        {"days": 1},
+        {"weeks": 1},
+        {"microseconds": -1},
+        {"milliseconds": -1},
+        {"seconds": -1},
+        {"minutes": -1},
+        {"hours": -1},
+        {"days": -1},
+        {"weeks": -1},
+        {"weeks": 1, "days": 2, "hours": 7, "minutes": 0, "seconds": -712}))
+    def test_set_durations(self, tmpdir, params):
+        cache = suds.cache.FileCache(tmpdir.strpath, **params)
+        assert cache.duration == datetime.timedelta(**params)
+
+    def test_version(self, tmpdir):
+        fake_version_info = "--- fake version info ---"
+        assert suds.__version__ != fake_version_info
+
+        version_file = tmpdir.join("version")
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.FileCache(cache_folder)
+        assert version_file.read() == suds.__version__
+        cache.put("unga1", value_p1)
+
+        version_file.write(fake_version_info)
+        assert cache.get("unga1") == value_p1
+
+        cache2 = suds.cache.FileCache(cache_folder)
+        _assert_empty_cache_folder(cache_folder)
+        assert cache.get("unga1") is None
+        assert cache2.get("unga1") is None
+        assert version_file.read() == suds.__version__
+        cache.put("unga1", value_p11)
+        cache.put("unga2", value_p22)
+
+        version_file.remove()
+        assert cache.get("unga1") == value_p11
+        assert cache.get("unga2") == value_p22
+
+        cache3 = suds.cache.FileCache(cache_folder)
+        _assert_empty_cache_folder(cache_folder)
+        assert cache.get("unga1") is None
+        assert cache.get("unga2") is None
+        assert cache2.get("unga1") is None
+        assert cache3.get("unga1") is None
+        assert version_file.read() == suds.__version__
+
+
+def test_NoCache(monkeypatch):
     cache = suds.cache.NoCache()
     assert isinstance(cache, suds.cache.Cache)
 
@@ -319,34 +895,158 @@ def test_NoCache():
     cache.put("id", "something")
     assert cache.get("id") == None
 
-    # TODO: It should not be an error to call purge() or clear() on a NoCache
+    #TODO: It should not be an error to call clear() or purge() on a NoCache
     # instance.
-    pytest.raises(Exception, cache.purge, "id")
-    pytest.raises(Exception, cache.clear)
-
-
-def test_ObjectCache(tmpdir):
-    cacheFolder = tmpdir.join("george carlin").strpath
-    cache = suds.cache.ObjectCache(cacheFolder)
-    assert isinstance(cache, suds.cache.FileCache)
-    assert cache.get("unga1") is None
-    assert cache.get("unga2") is None
-    cache.put("unga1", InvisibleMan(1))
-    cache.put("unga2", InvisibleMan(2))
-    read1 = cache.get("unga1")
-    read2 = cache.get("unga2")
-    assert read1.__class__ is InvisibleMan
-    assert read2.__class__ is InvisibleMan
-    assert read1.x == 1
-    assert read2.x == 2
-
-
-def _isEmptyCacheFolder(folder):
+    monkeypatch.delitem(locals(), "e", False)
+    e = pytest.raises(Exception, cache.purge, "id").value
+    try:
+        assert str(e) == "not-implemented"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
+    e = pytest.raises(Exception, cache.clear).value
+    try:
+        assert str(e) == "not-implemented"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
+
+
+class TestObjectCache:
+
+    def test_basic(self, tmpdir):
+        cache = suds.cache.ObjectCache(tmpdir.strpath)
+        assert isinstance(cache, suds.cache.FileCache)
+        assert cache.get("unga1") is None
+        assert cache.get("unga2") is None
+        cache.put("unga1", InvisibleMan(1))
+        cache.put("unga2", InvisibleMan(2))
+        read1 = cache.get("unga1")
+        read2 = cache.get("unga2")
+        assert read1.__class__ is InvisibleMan
+        assert read2.__class__ is InvisibleMan
+        assert read1.x == 1
+        assert read2.x == 2
+
+    def test_file_open_failure(self, tmpdir, monkeypatch):
+        """
+        File open failure should cause no cached object to be found, but any
+        existing underlying cache file should be kept around.
+
+        """
+        mock_open = MockFileOpener(fail_open=True)
+
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.ObjectCache(cache_folder)
+        cache.put("unga1", InvisibleMan(1))
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 1
+        _assert_empty_cache_folder(cache_folder, expected=False)
+        assert cache.get("unga1").x == 1
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga2") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 2
+        _assert_empty_cache_folder(cache_folder, expected=False)
+        assert cache.get("unga1").x == 1
+        assert cache.get("unga2") is None
+
+        cache.put("unga2", InvisibleMan(2))
+        assert mock_open.counter == 2
+
+        mock_open.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock_open.counter == 3
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+        assert cache.get("unga1").x == 1
+        assert cache.get("unga2").x == 2
+        assert mock_open.counter == 3
+
+    @pytest.mark.parametrize(("mock", "extra_checks"), (
+        (MockPickleLoad(), [lambda x: True] * 4),
+        (MockFileOpener(fail_read=True), [
+            lambda x: x.read_counter != 0,
+            lambda x: x.read_counter == 0,
+            lambda x: x.read_counter != 0,
+            lambda x: x.read_counter == 0])))
+    def test_file_operation_failure(self, tmpdir, monkeypatch, mock,
+            extra_checks):
+        """
+        Open file operation failures such as reading failures or failing to
+        unpickle the data read from such a file should cause no cached object
+        to be found and the related cache file to be removed.
+
+        """
+        cache_folder = tmpdir.strpath
+        cache = suds.cache.ObjectCache(cache_folder)
+        cache.put("unga1", InvisibleMan(1))
+
+        mock.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock.counter == 1
+        assert extra_checks[0](mock)
+        _assert_empty_cache_folder(cache_folder)
+
+        mock.reset()
+        assert cache.get("unga1") is None
+        cache.put("unga1", InvisibleMan(1))
+        cache.put("unga2", InvisibleMan(2))
+        assert mock.counter == 0
+        assert extra_checks[1](mock)
+
+        mock.reset()
+        mock.apply(monkeypatch)
+        assert cache.get("unga1") is None
+        monkeypatch.undo()
+        assert mock.counter == 1
+        assert extra_checks[2](mock)
+        _assert_empty_cache_folder(cache_folder, expected=False)
+
+        mock.reset()
+        assert cache.get("unga1") is None
+        assert cache.get("unga2").x == 2
+        assert mock.counter == 0
+        assert extra_checks[3](mock)
+
+    @pytest.mark.parametrize(("duration", "current_time", "expect_remove"),
+        file_cache_item_expiration_test_data)
+    def test_item_expiration(self, tmpdir, monkeypatch, duration, current_time,
+            expect_remove):
+        """See TestFileCache.item_expiration_test_worker() for more info."""
+        cache = suds.cache.ObjectCache(tmpdir.strpath, **duration)
+        cache.put("silly", InvisibleMan(666))
+        TestFileCache.item_expiration_test_worker(cache, "silly", monkeypatch,
+            current_time, expect_remove)
+
+
+def _assert_empty_cache_folder(folder, expected=True):
+    """Test utility asserting that a cache folder is or is not empty."""
+    if not _is_assert_enabled():
+        return
     assert os.path.isdir(folder)
-    def walkError(error):
-        pytest.fail("Error attempting to walk through cache folder contents.")
-    count = 0
-    for root, folders, files in os.walk(folder, onerror=walkError):
-        assert root == folder
-        return len(folders) == 0 and len(files) == 1 and files[0] == 'version'
+    def walk_error(error):
+        pytest.fail("Error walking through cache folder content.")
+    root, folders, files = next(os.walk(folder, onerror=walk_error))
+    assert root == folder
+    empty = len(folders) == 0 and len(files) == 1 and files[0] == 'version'
+    if expected:
+        assert len(folders) == 0
+        assert len(files) == 1
+        assert files[0] == 'version'
+        assert empty, "bad test code"
+    else:
+        assert not empty, "unexpected empty cache folder"
+
+
+def _is_assert_enabled():
+    """Return whether Python assertions have been enabled in this module."""
+    try:
+        assert False
+    except AssertionError:
+        return True
     return False
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..df8986f
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,726 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library suds.client.Client related unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+import testutils
+if __name__ == "__main__":
+    testutils.run_using_pytest(globals())
+
+import suds
+import suds.cache
+import suds.store
+import suds.transport
+import suds.transport.https
+
+import pytest
+from six import iteritems, itervalues, next
+from six.moves import http_client
+
+
+class MyException(Exception):
+    """Local exception class used in this testing module."""
+    pass
+
+
+class MockCache(suds.cache.Cache):
+    """
+    Mock cache structure used in the tests in this module.
+
+    Implements an in-memory cache and allows the test code to test the exact
+    triggered cache operations. May be configured to allow or not adding
+    additional entries to the cache, thus allowing our tests complete control
+    over the cache's content.
+
+    """
+
+    """Enumeration for specific mock operation configurations."""
+    ALLOW = 0
+    IGNORE = 1
+    FAIL = 2
+
+    def __init__(self):
+        self.mock_data = {}
+        self.mock_log = []
+        self.mock_put_config = MockCache.ALLOW
+        super(MockCache, self).__init__()
+
+    def clear(self):
+        self.mock_log.append(("clear", []))
+        pytest.fail("Unexpected MockCache.clear() operation call.")
+
+    def get(self, id):
+        self.mock_log.append(("get", [id]))
+        return self.mock_data.get(id, None)
+
+    def purge(self, id):
+        self.mock_log.append(("purge", [id]))
+        pytest.fail("Unexpected MockCache.purge() operation call.")
+
+    def put(self, id, object):
+        self.mock_log.append(("put", [id, object]))
+        if self.mock_put_config == MockCache.FAIL:
+            pytest.fail("Unexpected MockCache.put() operation call.")
+        if self.mock_put_config == MockCache.ALLOW:
+            self.mock_data[id] = object
+        else:
+            assert self.mock_put_config == MockCache.IGNORE
+
+
+class MockDocumentStore(suds.store.DocumentStore):
+    """Mock DocumentStore tracking all of its operations."""
+
+    def __init__(self, *args, **kwargs):
+        self.mock_log = []
+        self.mock_fail = kwargs.pop("mock_fail", False)
+        super(MockDocumentStore, self).__init__(*args, **kwargs)
+
+    def open(self, url):
+        self.mock_log.append(url)
+        if self.mock_fail:
+            raise MyException
+        return super(MockDocumentStore, self).open(url)
+
+    def reset(self):
+        self.mock_log = []
+
+
+class MockTransport(suds.transport.Transport):
+    """
+    Mock Transport used by the tests implemented in this module.
+
+    Allows the tests to check which transport operations got triggered and to
+    control what each of them returns.
+
+    open/send output data may be given either as a single value or a list of
+    values to be used in order. Specifying a single value is a shortcut with
+    the same semantics as specifying a single element list containing that
+    value. Each of the value items may be either a simple byte string to be
+    returned or an Exception subclass or instance indicating an exception to be
+    raised from a particular operation call.
+
+    """
+
+    def __init__(self, open_data=None, send_data=None):
+        if open_data is None:
+            open_data = []
+        elif open_data.__class__ is not list:
+            open_data = [open_data]
+        if send_data is None:
+            send_data = []
+        elif send_data.__class__ is not list:
+            send_data = [send_data]
+        self.mock_log = []
+        self.mock_open_data = open_data
+        self.mock_send_data = send_data
+        super(MockTransport, self).__init__()
+
+    def open(self, request):
+        self.mock_log.append(("open", [request.url]))
+        if not self.mock_open_data:
+            pytest.fail("Unexpected MockTransport.open() operation call.")
+        result = self.__next_operation_result(self.mock_open_data)
+        return suds.BytesIO(result)
+
+    def send(self, request):
+        self.mock_log.append(("send", [request.url, request.message]))
+        if not self.mock_send_data:
+            pytest.fail("Unexpected MockTransport.send() operation call.")
+        status = http_client.OK
+        headers = {}
+        data = self.__next_operation_result(self.mock_send_data)
+        return suds.transport.Reply(status, headers, data)
+
+    @staticmethod
+    def __next_operation_result(data_list):
+        value = data_list.pop(0)
+        if isinstance(value, Exception):
+            raise value
+        if value.__class__ is type and issubclass(value, Exception):
+            raise value()
+        assert value.__class__ is suds.byte_str_class, "bad test data"
+        return value
+
+
+# Test data used in different tests in this module testing suds WSDL schema
+# import implementation.
+#
+#TODO: Once a WSDL import bug illustrated by test_WSDL_import() is fixed, this
+# test data may be simplified to just:
+#   > wsdl_target_namespace = "bingo-bongo"
+#   > wsdl = testutils.wsdl("", wsdl_target_namespace=wsdl_target_namespace)
+#   > wsdl_wrapper = suds.byte_str("""\
+#   > <?xml version='1.0' encoding='UTF-8'?>
+#   > <definitions targetNamespace="%(tns)s"
+#   >     xmlns="http://schemas.xmlsoap.org/wsdl/">
+#   >   <import namespace="%(tns)s" location="suds://wsdl"/>
+#   > </definitions>""" % {"tns": wsdl_target_namespace})
+# This would also make caching the imported WSDL schema simpler as this makes
+# the imported WSDL schema usable without the extra importing wrapper as well.
+wsdl_imported_format = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="bye-bye"
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="ice-scream"
+        elementFormDefault="qualified"
+        attributeFormDefault="unqualified"
+        xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+%s
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>"""
+wsdl_import_wrapper_format = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="bye-bye"
+    xmlns:my_wsdl="bye-bye"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+  <wsdl:import namespace="bye-bye" location="%s"/>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f"/>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="my_wsdl:dummyPortType">
+    <soap:binding style="document"
+        transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="document"/>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="my_wsdl:dummy">
+      <soap:address location="unga-bunga-location"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>"""
+wsdl_imported_xsd_namespace = "ice-scream"
+
+
+# Test URL data used by several tests in this test module.
+test_URL_data = (
+    "sudo://make-me-a-sammich",
+    "http://my little URL",
+    "https://my little URL",
+    "xxx://my little URL",
+    "xxx:my little URL",
+    "xxx:")
+
+
+class TestCacheStoreTransportUsage:
+    """
+    suds.client.Client cache/store/transport component usage interaction tests.
+
+    """
+
+    class TestCachedWSDLObjectUsage:
+        """
+        Using a WSDL object read from the cache should not attempt to fetch any
+        of its referenced external documents from either the cache, the
+        document store or using the registered transport.
+
+        """
+
+        def test_avoid_imported_WSDL_fetching(self):
+            # Prepare data.
+            url_imported = "suds://wsdl_imported"
+            wsdl_import_wrapper = wsdl_import_wrapper_format % (url_imported,)
+            wsdl_import_wrapper = suds.byte_str(wsdl_import_wrapper)
+            wsdl_imported = suds.byte_str(wsdl_imported_format % ("",))
+
+            # Add to cache.
+            cache = MockCache()
+            store1 = MockDocumentStore(wsdl=wsdl_import_wrapper,
+                wsdl_imported=wsdl_imported)
+            c1 = suds.client.Client("suds://wsdl", cachingpolicy=1,
+                cache=cache, documentStore=store1, transport=MockTransport())
+            assert store1.mock_log == ["suds://wsdl", "suds://wsdl_imported"]
+            assert len(cache.mock_data) == 1
+            wsdl_object_id, wsdl_object = next(iteritems(cache.mock_data))
+            assert wsdl_object.__class__ is suds.wsdl.Definitions
+
+            # Reuse from cache.
+            cache.mock_log = []
+            store2 = MockDocumentStore(wsdl=wsdl_import_wrapper)
+            c2 = suds.client.Client("suds://wsdl", cachingpolicy=1,
+                cache=cache, documentStore=store2, transport=MockTransport())
+            assert cache.mock_log == [("get", [wsdl_object_id])]
+            assert store2.mock_log == []
+
+        def test_avoid_external_XSD_fetching(self):
+            # Prepare document content.
+            xsd_target_namespace = "balancana"
+            wsdl = testutils.wsdl("""\
+              <xsd:import schemaLocation="suds://imported_xsd"/>
+              <xsd:include schemaLocation="suds://included_xsd"/>""",
+                xsd_target_namespace=xsd_target_namespace)
+            external_xsd_format = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema">
+    <element name="external%d" type="string"/>
+</schema>"""
+            external_xsd1 = suds.byte_str(external_xsd_format % (1,))
+            external_xsd2 = suds.byte_str(external_xsd_format % (2,))
+
+            # Add to cache.
+            cache = MockCache()
+            store1 = MockDocumentStore(wsdl=wsdl, imported_xsd=external_xsd1,
+                included_xsd=external_xsd2)
+            c1 = suds.client.Client("suds://wsdl", cachingpolicy=1,
+                cache=cache, documentStore=store1, transport=MockTransport())
+            assert store1.mock_log == ["suds://wsdl", "suds://imported_xsd",
+                "suds://included_xsd"]
+            assert len(cache.mock_data) == 1
+            wsdl_object_id, wsdl_object = next(iteritems(cache.mock_data))
+            assert wsdl_object.__class__ is suds.wsdl.Definitions
+
+            # Reuse from cache.
+            cache.mock_log = []
+            store2 = MockDocumentStore(wsdl=wsdl)
+            c2 = suds.client.Client("suds://wsdl", cachingpolicy=1,
+                cache=cache, documentStore=store2, transport=MockTransport())
+            assert cache.mock_log == [("get", [wsdl_object_id])]
+            assert store2.mock_log == []
+
+    @pytest.mark.parametrize("importing_WSDL_cached", (False, True))
+    def test_importing_WSDL_from_cache_avoids_store_avoids_transport(self,
+            importing_WSDL_cached):
+        """
+        When a requested WSDL schema is located in the client's cache, it
+        should be read from there instead of fetching its data from the
+        client's document store or using its registered transport.
+
+        When it is is not located in the cache but can be found in the client's
+        document store, it should be fetched from there but not using the
+        client's registered transport.
+
+        Note that this test makes sense only when caching raw XML documents
+        (cachingpolicy == 0) and not when caching final WSDL objects
+        (cachingpolicy == 1).
+
+        """
+        # Prepare test data.
+        url_imported = "suds://wsdl_imported"
+        wsdl_import_wrapper = wsdl_import_wrapper_format % (url_imported,)
+        wsdl_import_wrapper = suds.byte_str(wsdl_import_wrapper)
+        wsdl_imported = suds.byte_str(wsdl_imported_format % (
+            '<xsd:element name="Pistachio" type="xsd:string"/>',))
+        wsdl_imported_element_id = ("Pistachio", wsdl_imported_xsd_namespace)
+
+        # Add to cache, making sure the imported WSDL schema is read from the
+        # document store and not fetched using the client's registered
+        # transport.
+        cache = MockCache()
+        store1 = MockDocumentStore(wsdl=wsdl_import_wrapper,
+            wsdl_imported=wsdl_imported)
+        c1 = suds.client.Client("suds://wsdl", cachingpolicy=0, cache=cache,
+            documentStore=store1, transport=MockTransport())
+        assert [x for x, y in cache.mock_log] == ["get", "put"] * 2
+        id_wsdl = cache.mock_log[0][1][0]
+        assert cache.mock_log[1][1][0] == id_wsdl
+        id_wsdl_imported = cache.mock_log[2][1][0]
+        assert cache.mock_log[3][1][0] == id_wsdl_imported
+        assert id_wsdl_imported != id_wsdl
+        assert store1.mock_log == ["suds://wsdl", "suds://wsdl_imported"]
+        assert len(cache.mock_data) == 2
+        wsdl_imported_document = cache.mock_data[id_wsdl_imported]
+        cached_definitions_element = wsdl_imported_document.root().children[0]
+        cached_schema_element = cached_definitions_element.children[0]
+        cached_external_element = cached_schema_element.children[0]
+        schema = c1.wsdl.schema
+        external_element = schema.elements[wsdl_imported_element_id].root
+        assert cached_external_element is external_element
+
+        # Import the WSDL schema from the cache without fetching it using the
+        # document store or the transport.
+        cache.mock_log = []
+        if importing_WSDL_cached:
+            cache.mock_put_config = MockCache.FAIL
+            store2 = MockDocumentStore(mock_fail=True)
+        else:
+            del cache.mock_data[id_wsdl]
+            assert len(cache.mock_data) == 1
+            store2 = MockDocumentStore(wsdl=wsdl_import_wrapper)
+        c2 = suds.client.Client("suds://wsdl", cachingpolicy=0, cache=cache,
+            documentStore=store2, transport=MockTransport())
+        expected_cache_operations = [("get", id_wsdl)]
+        if not importing_WSDL_cached:
+            expected_cache_operations.append(("put", id_wsdl))
+        expected_cache_operations.append(("get", id_wsdl_imported))
+        cache_operations = [(x, y[0]) for x, y in cache.mock_log]
+        assert cache_operations == expected_cache_operations
+        if not importing_WSDL_cached:
+            assert store2.mock_log == ["suds://wsdl"]
+        assert len(cache.mock_data) == 2
+        assert cache.mock_data[id_wsdl_imported] is wsdl_imported_document
+        schema = c2.wsdl.schema
+        external_element = schema.elements[wsdl_imported_element_id].root
+        assert cached_external_element is external_element
+
+    @pytest.mark.parametrize("caching_policy", (0, 1))
+    def test_using_cached_WSDL_avoids_store_avoids_transport(self,
+            caching_policy):
+        """
+        When a client's WSDL schema is located in the cache, it should be read
+        from there instead of fetching its data from the client's document
+        store or using its registered transport.
+
+        When it is is not located in the cache but can be found in the client's
+        document store, it should be fetched from there but not using the
+        client's registered transport.
+
+        """
+        # Add to cache, making sure the WSDL schema is read from the document
+        # store and not fetched using the client's registered transport.
+        cache = MockCache()
+        store1 = MockDocumentStore(umpala=testutils.wsdl(""))
+        c1 = suds.client.Client("suds://umpala", cachingpolicy=caching_policy,
+            cache=cache, documentStore=store1, transport=MockTransport())
+        assert [x for x, y in cache.mock_log] == ["get", "put"]
+        id = cache.mock_log[0][1][0]
+        assert id == cache.mock_log[1][1][0]
+        assert len(cache.mock_data) == 1
+        if caching_policy == 0:
+            # Cache contains SAX XML documents.
+            wsdl_document = next(itervalues(cache.mock_data))
+            assert wsdl_document.__class__ is suds.sax.document.Document
+            wsdl_cached_root = wsdl_document.root()
+        else:
+            # Cache contains complete suds WSDL objects.
+            wsdl = next(itervalues(cache.mock_data))
+            assert wsdl.__class__ is suds.wsdl.Definitions
+            wsdl_cached_root = wsdl.root
+        assert c1.wsdl.root is wsdl_cached_root
+
+        # Make certain the same WSDL schema is fetched from the cache and not
+        # using the document store or the transport.
+        cache.mock_log = []
+        cache.mock_put_config = MockCache.FAIL
+        store2 = MockDocumentStore(mock_fail=True)
+        c2 = suds.client.Client("suds://umpala", cachingpolicy=caching_policy,
+            cache=cache, documentStore=store2, transport=MockTransport())
+        assert cache.mock_log == [("get", [id])]
+        assert c2.wsdl.root is wsdl_cached_root
+
+    @pytest.mark.parametrize("external_reference_tag", ("import", "include"))
+    @pytest.mark.parametrize("main_WSDL_cached", (False, True))
+    def test_using_cached_XSD_schema_avoids_store_avoids_transport(self,
+            external_reference_tag, main_WSDL_cached):
+        """
+        When an imported or included XSD schema is located in the client's
+        cache, it should be read from there instead of fetching its data from
+        the client's document store or using its registered transport.
+
+        When it is is not located in the cache but can be found in the client's
+        document store, it should be fetched from there but not using the
+        client's registered transport.
+
+        Note that this test makes sense only when caching raw XML documents
+        (cachingpolicy == 0) and not when caching final WSDL objects
+        (cachingpolicy == 1).
+
+        """
+        # Prepare document content.
+        xsd_target_namespace = "my xsd namespace"
+        wsdl = testutils.wsdl('<xsd:%s schemaLocation="suds://external"/>' % (
+            external_reference_tag,),
+            xsd_target_namespace=xsd_target_namespace)
+        external_schema = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema">
+  <element name="external" type="string"/>
+</schema>
+""")
+
+        # Imported XSD schema items retain their namespace, while included ones
+        # get merged into the target namespace.
+        external_element_namespace = None
+        if external_reference_tag == "include":
+            external_element_namespace = xsd_target_namespace
+        external_element_id = ("external", external_element_namespace)
+
+        # Add to cache.
+        cache = MockCache()
+        store1 = MockDocumentStore(wsdl=wsdl, external=external_schema)
+        c1 = suds.client.Client("suds://wsdl", cachingpolicy=0, cache=cache,
+            documentStore=store1, transport=MockTransport())
+        assert [x for x, y in cache.mock_log] == ["get", "put"] * 2
+        id_wsdl = cache.mock_log[0][1][0]
+        assert id_wsdl == cache.mock_log[1][1][0]
+        id_xsd = cache.mock_log[2][1][0]
+        assert id_xsd == cache.mock_log[3][1][0]
+        assert len(cache.mock_data) == 2
+        wsdl_document = cache.mock_data[id_wsdl]
+        assert c1.wsdl.root is wsdl_document.root()
+        # Making sure id_xsd refers to the actual external XSD is a bit tricky
+        # due to the fact that the WSDL object merged in the external XSD
+        # content and lost the reference to the external XSD object itself. As
+        # a workaround we make sure that the XSD schema XML element read from
+        # the XSD object cached as id_xsd matches the one read from the WSDL
+        # object's XSD schema.
+        xsd_imported_document = cache.mock_data[id_xsd]
+        cached_external_element = xsd_imported_document.root().children[0]
+        external_element = c1.wsdl.schema.elements[external_element_id].root
+        assert cached_external_element is external_element
+
+        # Make certain the same external XSD document is fetched from the cache
+        # and not using the document store or the transport.
+        cache.mock_log = []
+        if main_WSDL_cached:
+            cache.mock_put_config = MockCache.FAIL
+            store2 = MockDocumentStore(mock_fail=True)
+        else:
+            del cache.mock_data[id_wsdl]
+            assert len(cache.mock_data) == 1
+            store2 = MockDocumentStore(wsdl=wsdl)
+        c2 = suds.client.Client("suds://wsdl", cachingpolicy=0, cache=cache,
+            documentStore=store2, transport=MockTransport())
+        expected_cache_operations = [("get", id_wsdl)]
+        if not main_WSDL_cached:
+            expected_cache_operations.append(("put", id_wsdl))
+        expected_cache_operations.append(("get", id_xsd))
+        cache_operations = [(x, y[0]) for x, y in cache.mock_log]
+        assert cache_operations == expected_cache_operations
+        if not main_WSDL_cached:
+            assert store2.mock_log == ["suds://wsdl"]
+        assert len(cache.mock_data) == 2
+        assert cache.mock_data[id_xsd] is xsd_imported_document
+        external_element = c2.wsdl.schema.elements[external_element_id].root
+        assert cached_external_element is external_element
+
+
+class TestCacheUsage:
+    """suds.client.Client cache component usage tests."""
+
+    @pytest.mark.parametrize("cache", (
+        None,
+        suds.cache.NoCache(),
+        suds.cache.ObjectCache()))
+    def test_avoiding_default_cache_construction(self, cache, monkeypatch):
+        """Explicitly specified cache avoids default cache construction."""
+        def construct_default_cache(*args, **kwargs):
+            pytest.fail("Unexpected default cache instantiation.")
+        class MockStore(suds.store.DocumentStore):
+            def open(self, *args, **kwargs):
+                raise MyException
+        monkeypatch.setattr(suds.cache, "ObjectCache", construct_default_cache)
+        monkeypatch.setattr(suds.store, "DocumentStore", MockStore)
+        pytest.raises(MyException, suds.client.Client, "suds://some_URL",
+            documentStore=MockStore(), cache=cache)
+
+    def test_default_cache_construction(self, monkeypatch):
+        """
+        Test when and how client creates its default cache object.
+
+        We use a dummy store to get an expected exception rather than
+        attempting to access the network, in case the test fails and the
+        expected default cache object does not get created or gets created too
+        late.
+
+        """
+        def construct_default_cache(days):
+            assert days == 1
+            raise MyException
+        class MockStore(suds.store.DocumentStore):
+            def open(self, *args, **kwargs):
+                pytest.fail("Default cache not created in time.")
+        monkeypatch.setattr(suds.cache, "ObjectCache", construct_default_cache)
+        monkeypatch.setattr(suds.store, "DocumentStore", MockStore)
+        pytest.raises(MyException, suds.client.Client, "suds://some_URL",
+            documentStore=MockStore())
+
+    @pytest.mark.parametrize("cache", (object(), MyException()))
+    def test_reject_invalid_cache_class(self, cache, monkeypatch):
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(AttributeError, suds.client.Client,
+            "suds://some_URL", cache=cache).value
+        try:
+            expected_error = '"cache" must be: (%r,)'
+            assert str(e) == expected_error % (suds.cache.Cache,)
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestStoreUsage:
+    """suds.client.Client document store component usage tests."""
+
+    @pytest.mark.parametrize("store", (object(), suds.cache.NoCache()))
+    def test_reject_invalid_store_class(self, store, monkeypatch):
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(AttributeError, suds.client.Client,
+            "suds://some_URL", documentStore=store, cache=None).value
+        try:
+            expected_error = '"documentStore" must be: (%r,)'
+            assert str(e) == expected_error % (suds.store.DocumentStore,)
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestTransportUsage:
+    """suds.client.Client transport component usage tests."""
+
+    def test_default_transport(self):
+        client = testutils.client_from_wsdl(testutils.wsdl(""))
+        expected = suds.transport.https.HttpAuthenticated
+        assert client.options.transport.__class__ is expected
+
+    @pytest.mark.parametrize("exception", (
+        MyException(),  # non-TransportError exception
+        suds.transport.TransportError("huku", 666)))
+    def test_error_on_open(self, monkeypatch, exception):
+        monkeypatch.delitem(locals(), "e_info", False)
+        transport = MockTransport(open_data=exception)
+        e_info = pytest.raises(exception.__class__, suds.client.Client, "url",
+            cache=None, transport=transport)
+        try:
+            assert e_info.value is exception
+        finally:
+            del e_info  # explicitly break circular reference chain in Python 3
+
+    def test_error_on_send__non_transport(self):
+        e = MyException()
+        t = MockTransport(send_data=e)
+        store = MockDocumentStore(wsdl=testutils.wsdl("", operation_name="g"))
+        client = suds.client.Client("suds://wsdl", documentStore=store,
+            cache=None, transport=t)
+        assert pytest.raises(MyException, client.service.g).value is e
+
+    #TODO: The test_error_on_send__transport() test should be made much more
+    # detailed. suds.transport.TransportError exceptions get handled in a much
+    # more complex way then is demonstrated here, for example:
+    #  - some HTTP status codes result in an exception being raised and some in
+    #    different handling
+    #  - an exception may be raised or returned depending no the
+    #    suds.options.faults suds option
+    # Also, the whole concept of re-raising a TransportError exception as a
+    # simple Exception instance seems like bad design. For now this rough test
+    # just demonstrates that suds.transport.TransportError exceptions get
+    # handled differently from other exception types.
+    def test_error_on_send__transport(self, monkeypatch):
+        monkeypatch.delitem(locals(), "e", False)
+        t = MockTransport(send_data=suds.transport.TransportError("huku", 666))
+        store = MockDocumentStore(wsdl=testutils.wsdl("", operation_name="g"))
+        client = suds.client.Client("suds://wsdl", documentStore=store,
+            cache=None, transport=t)
+        e = pytest.raises(Exception, client.service.g).value
+        try:
+            assert e.__class__ is Exception
+            assert e.args == ((666, "huku"),)
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    def test_nosend_should_avoid_transport_sends(self):
+        wsdl = testutils.wsdl("")
+        t = MockTransport()
+        client = testutils.client_from_wsdl(wsdl, nosend=True, transport=t)
+        client.service.f()
+
+    def test_operation_request_and_reply(self):
+        xsd_content = '<xsd:element name="Data" type="xsd:string"/>'
+        web_service_URL = "Great minds think alike"
+        xsd_target_namespace = "omicron psi"
+        wsdl = testutils.wsdl(suds.byte_str(xsd_content), operation_name="pi",
+            xsd_target_namespace=xsd_target_namespace, input="Data",
+            output="Data", web_service_URL=web_service_URL)
+        test_input_data = "Riff-raff"
+        test_output_data = "La-di-da-da-da"
+        store = MockDocumentStore(wsdl=wsdl)
+        transport = MockTransport(send_data=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <Data xmlns="%s">%s</Data>
+  </env:Body>
+</env:Envelope>""" % (xsd_target_namespace, test_output_data)))
+        client = suds.client.Client("suds://wsdl", documentStore=store,
+            cache=None, transport=transport)
+        assert transport.mock_log == []
+        reply = client.service.pi(test_input_data)
+        assert len(transport.mock_log) == 1
+        assert transport.mock_log[0][0] == "send"
+        assert transport.mock_log[0][1][0] == web_service_URL
+        request_message = transport.mock_log[0][1][1]
+        assert suds.byte_str(xsd_target_namespace) in request_message
+        assert suds.byte_str(test_input_data) in request_message
+        assert reply == test_output_data
+
+    @pytest.mark.parametrize("transport", (object(), suds.cache.NoCache()))
+    def test_reject_invalid_transport_class(self, transport, monkeypatch):
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(AttributeError, suds.client.Client,
+            "suds://some_URL", transport=transport, cache=None).value
+        try:
+            expected_error = '"transport" must be: (%r,)'
+            assert str(e) == expected_error % (suds.transport.Transport,)
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    @pytest.mark.parametrize("url", test_URL_data)
+    def test_WSDL_transport(self, url):
+        store = MockDocumentStore()
+        t = MockTransport(open_data=testutils.wsdl(""))
+        suds.client.Client(url, cache=None, documentStore=store, transport=t)
+        assert t.mock_log == [("open", [url])]
+
+    @pytest.mark.parametrize("url", test_URL_data)
+    def test_imported_WSDL_transport(self, url):
+        wsdl_import_wrapper = wsdl_import_wrapper_format % (url,)
+        wsdl_imported = suds.byte_str(wsdl_imported_format % ("",))
+        store = MockDocumentStore(wsdl=suds.byte_str(wsdl_import_wrapper))
+        t = MockTransport(open_data=wsdl_imported)
+        suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+            transport=t)
+        assert t.mock_log == [("open", [url])]
+
+    @pytest.mark.parametrize("url", test_URL_data)
+    @pytest.mark.parametrize("external_reference_tag", ("import", "include"))
+    def test_external_XSD_transport(self, url, external_reference_tag):
+        xsd_content = '<xsd:%(tag)s schemaLocation="%(url)s"/>' % dict(
+            tag=external_reference_tag, url=url)
+        store = MockDocumentStore(wsdl=testutils.wsdl(xsd_content))
+        t = MockTransport(open_data=suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"/>
+"""))
+        suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+            transport=t)
+        assert t.mock_log == [("open", [url])]
+
+
+ at pytest.mark.xfail(reason="WSDL import buggy")
+def test_WSDL_import():
+    wsdl_target_namespace = "bingo-bongo"
+    wsdl = testutils.wsdl("", wsdl_target_namespace=wsdl_target_namespace)
+    wsdl_wrapper = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<definitions targetNamespace="%(tns)s"
+    xmlns="http://schemas.xmlsoap.org/wsdl/">
+  <import namespace="%(tns)s" location="suds://wsdl"/>
+</definitions>""" % {"tns": wsdl_target_namespace})
+    store = suds.store.DocumentStore(wsdl=wsdl, wsdl_wrapper=wsdl_wrapper)
+    client = suds.client.Client("suds://wsdl_wrapper", documentStore=store,
+        cache=None, nosend=True)
+    client.service.f()
+    #TODO: client.service is empty but other parts of client's imported WSDL
+    # data, e.g. port_type, are there so my guess is that this is something
+    # that was intended to work. (19.02.2014.) (Jurko)
+    #TODO: Look into the exact client.wsdl.schema content. Its string
+    # representation does not seem to be valid.
diff --git a/tests/test_client_cache.py b/tests/test_client_cache.py
deleted file mode 100644
index 0270bf1..0000000
--- a/tests/test_client_cache.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
-
-"""
-Suds Python library client cache related unit tests.
-
-Implemented using the 'pytest' testing framework.
-
-"""
-
-if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
-
-import suds
-import suds.cache
-import suds.store
-
-import pytest
-
-
-class MyException(Exception):
-    """Local exception class used in this testing module."""
-    pass
-
-
-def test_default_cache_construction(monkeypatch):
-    """
-    Test when and how client creates its default cache object.
-
-    We use a dummy store to get an expected exception rather than attempting to
-    access the network, in case the test fails and the expected default cache
-    object does not get created or gets created too late.
-
-    """
-    def constructDefaultCache(days):
-        assert days == 1
-        raise MyException
-    class MockStore(suds.store.DocumentStore):
-        def open(self, *args, **kwargs):
-            pytest.fail("Default cache not created in time.")
-    monkeypatch.setattr("suds.client.ObjectCache", constructDefaultCache)
-    monkeypatch.setattr("suds.store.DocumentStore", MockStore)
-    pytest.raises(MyException, suds.client.Client, "some_url",
-        documentStore=MockStore())
-
-
- at pytest.mark.parametrize("cache", (
-    None,
-    suds.cache.NoCache(),
-    suds.cache.ObjectCache()))
-def test_avoiding_default_cache(cache, monkeypatch):
-    """Explicitly specified cache should avoid default cache construction."""
-    def constructDefaultCache(*args, **kwargs):
-        pytest.fail("Unexpected default cache instantiation.")
-    class MockStore(suds.store.DocumentStore):
-        def open(self, *args, **kwargs):
-            raise MyException
-    monkeypatch.setattr("suds.client.ObjectCache", constructDefaultCache)
-    monkeypatch.setattr("suds.store.DocumentStore", MockStore)
-    pytest.raises(MyException, suds.client.Client, "some_url",
-        documentStore=MockStore(), cache=cache)
diff --git a/tests/test_compare_sax.py b/tests/test_compare_sax.py
new file mode 100644
index 0000000..3e9703a
--- /dev/null
+++ b/tests/test_compare_sax.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+CompareSAX testing utility unit tests.
+
+"""
+
+if __name__ == "__main__":
+    import testutils
+    testutils.run_using_pytest(globals())
+
+import suds
+import suds.sax.document
+import suds.sax.parser
+from testutils.assertion import assert_no_output
+from testutils.compare_sax import CompareSAX
+
+import pytest
+from six import text_type, u
+
+import xml.sax
+
+
+# CompareSAX class uses Python assertions to report failed comparison results
+# so we need to skip the tests in this module if Python assertions have been
+# disabled in the CompareSAX implementation module.
+skip_test_if_CompareSAX_assertions_disabled = pytest.mark.skipif(
+    not CompareSAX.assertions_enabled(),
+    reason="CompareSAX assertions disabled")
+
+
+ at skip_test_if_CompareSAX_assertions_disabled
+ at pytest.mark.parametrize("data", (
+    "",
+    "<bad1/><bad2/>",
+    '<bad a="1" a="1"/>',
+    "<bad><bad>xml</document></bad>"))
+def test_failed_parsing(data, capsys):
+    pytest.raises(xml.sax.SAXParseException, CompareSAX.data2data, data, data)
+    assert_no_output(capsys)
+
+
+class TestMatched:
+    """Successful CompareSAX matching tests."""
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_empty_document(self, capsys):
+        a = suds.sax.document.Document()
+        b = suds.sax.document.Document()
+        CompareSAX.document2document(a, b)
+        assert_no_output(capsys)
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    @pytest.mark.parametrize(("data1", "data2"), (
+        # Simple matches.
+        ('<a><ns:b xmlns:ns="x"/></a>', '<a><ns:b xmlns:ns="x"/></a>'),
+        ('<a><b xmlns="x"/></a>', '<a><b xmlns="x"/></a>'),
+        ('<a xmlns="x"><b/></a>', '<a xmlns="x"><b/></a>'),
+        # Extra namespace declarations.
+        ('<ns1:b xmlns:ns1="two"/>', '<ns2:b xmlns="one" xmlns:ns2="two"/>'),
+        ('<ns1:b xmlns:ns1="2"/>', '<ns2:b xmlns:ns3="1" xmlns:ns2="2"/>'),
+        ('<b xmlns="1"/>', '<ns1:b xmlns="0" xmlns:ns1="1" xmlns:ns2="2"/>'),
+        # Mismatched namespace prefixes.
+        ('<a xmlns="one"/>', '<ns:a xmlns:ns="one"/>'),
+        ('<ns1:b xmlns:ns1="two"/>', '<ns2:b xmlns:ns2="two"/>'),
+        # Numeric unicode character references.
+        (u("<a>\u2606</a>"), "<a>&#%d;</a>" % (0x2606,))))
+    def test_data2data(self, data1, data2, capsys):
+        CompareSAX.data2data(data1, data2)
+        assert_no_output(capsys)
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    @pytest.mark.parametrize("type1", (suds.byte_str, text_type))
+    @pytest.mark.parametrize("type2", (suds.byte_str, text_type))
+    def test_string_input_types(self, type1, type2, capsys):
+        xml = "<a/>"
+        CompareSAX.data2data(type1(xml), type2(xml))
+        assert_no_output(capsys)
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_xml_encoding(self, capsys):
+        """Test that the encoding listed in the XML declaration is honored."""
+        xml_format = u('<?xml version="1.0" encoding="%s"?><a>\u00D8</a>')
+        data1 = (xml_format % ("UTF-8",)).encode('utf-8')
+        data2 = (xml_format % ("latin1",)).encode('latin1')
+        CompareSAX.data2data(data1, data2)
+        assert_no_output(capsys)
+
+
+class TestMismatched:
+    """Failed CompareSAX matching tests."""
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    @pytest.mark.parametrize(("data1", "data2", "expected_context"), (
+        # Different element namespaces.
+        ("<a/>", '<a xmlns="x"/>', "data2data.<a>.namespace"),
+        ('<a xmlns="1"/>', '<a xmlns="2"/>', "data2data.<a>.namespace"),
+        ('<r><a xmlns="1"/></r>', '<r><a xmlns="2"/></r>',
+            "data2data.<r>.<a>.namespace"),
+        ('<r><tag><a xmlns="1"/></tag><y/></r>',
+            '<r><tag><a xmlns="2"/></tag><y/></r>',
+            "data2data.<r>.<tag(1/2)>.<a>.namespace"),
+        # Different textual content in text only nodes.
+        ("<a>one</a>", "<a>two</a>", "data2data.<a>.text"),
+        ("<a>x</a>", "<a>x </a>", "data2data.<a>.text"),
+        ("<a>x</a>", "<a>x  </a>", "data2data.<a>.text"),
+        ("<a>x </a>", "<a>x  </a>", "data2data.<a>.text"),
+        ("<a> x</a>", "<a>x</a>", "data2data.<a>.text"),
+        ("<a>  x</a>", "<a>x</a>", "data2data.<a>.text"),
+        ("<a>  x</a>", "<a> x</a>", "data2data.<a>.text"),
+        ("<a><b><c>x</c><c2/></b></a>", "<a><b><c>X</c><c2/></b></a>",
+            "data2data.<a>.<b>.<c(1/2)>.text"),
+        ("<a><b><c>x</c><d>y</d></b></a>", "<a><b><c>x</c><d>Y</d></b></a>",
+            "data2data.<a>.<b>.<d(2/2)>.text"),
+        # Different textual content in mixed content nodes with children.
+        ("<a>42<b/><b/>42</a>", "<a>42<b/> <b/>42</a>", "data2data.<a>.text"),
+        # Differently named elements.
+        ("<a/>", "<b/>", "data2data.<a/b>"),
+        ("<a><b/></a>", "<a><c/></a>", "data2data.<a>.<b/c>"),
+        ("<a><b/><x/></a>", "<a><c/><x/></a>", "data2data.<a>.<b/c(1/2)>"),
+        ("<a><x/><b/></a>", "<a><x/><c/></a>", "data2data.<a>.<b/c(2/2)>"),
+        ("<a><b><c/></b></a>", "<a><b><d/></b></a>",
+            "data2data.<a>.<b>.<c/d>"),
+        ("<a><b><y1/><y2/><c/></b><x/></a>",
+            "<a><b><y1/><y2/><d/></b><x/></a>",
+            "data2data.<a>.<b(1/2)>.<c/d(3/3)>"),
+        # Extra/missing non-root element.
+        ("<a><b/></a>", "<a/>", "data2data.<a>"),
+        ("<a/>", "<a><b/></a>", "data2data.<a>"),
+        ("<a><x/><b/></a>", "<a><b/></a>", "data2data.<a>"),
+        ("<a><b/><x/></a>", "<a><b/></a>", "data2data.<a>"),
+        ("<a><b/></a>", "<a><x/><b/></a>", "data2data.<a>"),
+        ("<a><b/></a>", "<a><b/><x/></a>", "data2data.<a>"),
+        # Multiple differences.
+        ("<a><b/></a>", "<c><d/></c>", "data2data.<a/c>"),
+        ("<a><b/></a>", '<a xmlns="o"><c/></a>', "data2data.<a>.namespace"),
+        ("<r><a><b/></a></r>", "<r><c><d/></c></r>", "data2data.<r>.<a/c>"),
+        ("<r><a><b/></a></r>", '<r><a xmlns="o"><c/></a></r>',
+            "data2data.<r>.<a>.namespace")))
+    def test_data2data(self, data1, data2, expected_context, capsys):
+        pytest.raises(AssertionError, CompareSAX.data2data, data1, data2)
+        _assert_context_output(capsys, expected_context)
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_document2document_context(self, capsys):
+        a = suds.sax.document.Document()
+        b = suds.sax.parser.Parser().parse(string=suds.byte_str("<a/>"))
+        pytest.raises(AssertionError, CompareSAX.document2document, a, b)
+        _assert_context_output(capsys, "document2document")
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_document2element_context(self, capsys):
+        a = suds.sax.parser.Parser().parse(string=suds.byte_str("<xx>1</xx>"))
+        b = suds.sax.parser.Parser().parse(string=suds.byte_str("<xx>2</xx>"))
+        pytest.raises(AssertionError, CompareSAX.document2element, a, b.root())
+        _assert_context_output(capsys, "document2element.<xx>.text")
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_element2element_context(self, capsys):
+        Parser = suds.sax.parser.Parser
+        e1 = Parser().parse(string=suds.byte_str("<x/>")).root()
+        e2 = Parser().parse(string=suds.byte_str("<y/>")).root()
+        pytest.raises(AssertionError, CompareSAX.element2element, e1, e2)
+        _assert_context_output(capsys, "element2element.<x/y>")
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_element2element_context_invalid_name__left(self, capsys):
+        Parser = suds.sax.parser.Parser
+        e = Parser().parse(string=suds.byte_str("<x/>")).root()
+        e_invalid = object()
+        pytest.raises(AssertionError, CompareSAX.element2element, e_invalid, e)
+        _assert_context_output(capsys, "element2element.<???/x>")
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_element2element_context_invalid_name__right(self, capsys):
+        Parser = suds.sax.parser.Parser
+        e = Parser().parse(string=suds.byte_str("<y/>")).root()
+        e_invalid = object()
+        pytest.raises(AssertionError, CompareSAX.element2element, e, e_invalid)
+        _assert_context_output(capsys, "element2element.<y/???>")
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    def test_empty_vs_non_empty_document(self, capsys):
+        document = suds.sax.document.Document()
+        data = "<a/>"
+        pytest.raises(AssertionError, CompareSAX.document2data, document, data)
+        _assert_context_output(capsys, "document2data")
+
+
+#TODO: TestSAXModelFeatures tests should be removed once their respective SAX
+# document model features get tested by SAX document model specific unit tests.
+#TODO: Additional missing suds SAX document model unit tests:
+#  * SAX parser fails on documents with multiple root elements.
+#  * SAX document may contain at most one element, accessible as root().
+#  * SAX document append() overwrites the root element silently.
+class TestSAXModelFeatures:
+    """SAX document model feature testing using the CompareSAX interface."""
+
+    @skip_test_if_CompareSAX_assertions_disabled
+    @pytest.mark.parametrize(("data1", "data2"), (
+        # Differently placed default namespace declaration.
+        ('<ns:a xmlns:ns="1" xmlns="2"><b/></ns:a>',
+        '<ns:a xmlns:ns="1"><b xmlns="2"/></ns:a>'),
+        # Differently placed namespace prefix declaration.
+        ('<a xmlns:ns="1"><ns:b/></a>', '<a><ns:b xmlns:ns="1"/></a>'),
+        # Element's textual content merged.
+        ("<a>111<b/>222</a>", "<a>111222<b/></a>"),
+        ("<a>111<b/>222</a>", "<a><b/>111222</a>"),
+        ("<a>111<b/>222</a>", "<a>11<b/>1222</a>"),
+        # Explicit "" namespace == no prefix or default namespace.
+        ('<a xmlns=""/>', "<a/>"),
+        ('<ns:a xmlns:ns=""/>', "<a/>"),
+        # Extra leading/trailing textual whitespace trimmed in mixed content
+        # elements with more than one child element.
+        ("<a>   \n\n <b/> \t\t\n\n</a>", "<a><b/></a>"),
+        ("<a>   \nxxx\n <b/> \t\t\n\n</a>", "<a>xxx<b/></a>")))
+    def test_data2data(self, data1, data2, capsys):
+        CompareSAX.data2data(data1, data2)
+        assert_no_output(capsys)
+
+
+def _assert_context_output(capsys, context):
+    """
+    Test utility asserting an expected captured stderr context output and no
+    captured stdout output.
+
+    """
+    out, err = capsys.readouterr()
+    assert not out
+    assert err == "Failed SAX XML comparison context:\n  %s\n" % (context,)
diff --git a/tests/test_date_time.py b/tests/test_date_time.py
index dc55a71..afcebe0 100644
--- a/tests/test_date_time.py
+++ b/tests/test_date_time.py
@@ -21,14 +21,11 @@ Implemented using the 'pytest' testing framework.
 """
 
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    import testutils
+    testutils.run_using_pytest(globals())
 
 from suds.sax.date import (FixedOffsetTimezone, Date, DateTime, Time,
     UtcTimezone)
-from suds.xsd.sxbuiltin import XDate, XDateTime, XTime
-import tests
 
 import pytest
 
@@ -228,7 +225,7 @@ class TestDateTime:
         ("2013-12-31T23:59:59.99999949", 2013, 12, 31, 23, 59, 59, 999999),
         ("2013-12-31T23:59:59.9999995", 2014, 1, 1, 0, 0, 0, 0)))
     def testConstructFromString_subsecondRounding(self, string, y, M, d, h, m,
-        s, micros):
+            s, micros):
         ref = datetime.datetime(y, M, d, h, m, s, micros)
         assert DateTime(string).value == ref
 
@@ -245,7 +242,7 @@ class TestDateTime:
         ("2013-11-19T14:05:23.428068-23:59",
             2013, 11, 19, 14, 5, 23, 428068, -23, -59)))
     def testConstructFromString_timezone(self, string, y, M, d, h, m, s,
-        micros, tz_h, tz_m):
+            micros, tz_h, tz_m):
         tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
         tzinfo = FixedOffsetTimezone(tzdelta)
         ref = datetime.datetime(y, M, d, h, m, s, micros, tzinfo=tzinfo)
@@ -323,7 +320,7 @@ class TestTime:
         ("0:0:0.9999996", 0, 0, 1, 0),
         ("0:0:0.9999999", 0, 0, 1, 0)))
     def testConstructFromString_subsecondRounding(self, string, h, m, s,
-        micros):
+            micros):
         assert Time(string).value == datetime.time(h, m, s, micros)
 
     @pytest.mark.parametrize(
@@ -337,7 +334,7 @@ class TestTime:
         ("18:0:09.2139+10:31", 18, 0, 9, 213900, 10, 31),
         ("18:0:09.2139-10:31", 18, 0, 9, 213900, -10, -31)))
     def testConstructFromString_timezone(self, string, h, m, s, micros, tz_h,
-        tz_m):
+            tz_m):
         tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
         tzinfo = FixedOffsetTimezone(tzdelta)
         ref = datetime.time(h, m, s, micros, tzinfo=tzinfo)
@@ -363,101 +360,3 @@ class TestTime:
         ("14:05:23.428-23:59", "14:05:23.428000-23:59")))
     def testConvertToString(self, input, output):
         assert str(Time(input)) == output
-
-
-class TestXDate:
-    """
-    Tests for the suds.xsd.sxbuiltin.XDate class.
-
-    Python object <--> string conversion details already tested in TestDate.
-
-    """
-
-    def testTranslateEmptyStringToPythonObject(self):
-        assert XDate.translate("") == None
-
-    def testTranslateStringToPythonObject(self):
-        assert XDate.translate("1941-12-7") == datetime.date(1941, 12, 7)
-
-    def testTranslatePythonObjectToString(self):
-        date = datetime.date(2013, 7, 24)
-        translated = XDate.translate(date, topython=False)
-        assert isinstance(translated, str)
-        assert translated == "2013-07-24"
-
-    def testTranslatePythonObjectToString_datetime(self):
-        dt = datetime.datetime(2013, 7, 24, 11, 59, 4)
-        translated = XDate.translate(dt, topython=False)
-        assert isinstance(translated, str)
-        assert translated == "2013-07-24"
-
-    @pytest.mark.parametrize("source", (
-        None,
-        object(),
-        _Dummy(),
-        datetime.time()))
-    def testTranslatePythonObjectToString_failed(self, source):
-        assert XDate.translate(source, topython=False) is source
-
-
-class TestXDateTime:
-    """
-    Tests for the suds.xsd.sxbuiltin.XDateTime class.
-
-    Python object <--> string conversion details already tested in
-    TestDateTime.
-
-    """
-
-    def testTranslateEmptyStringToPythonObject(self):
-        assert XDateTime.translate("") == None
-
-    def testTranslateStringToPythonObject(self):
-        dt = datetime.datetime(1941, 12, 7, 10, 30, 22, 454000)
-        assert XDateTime.translate("1941-12-7T10:30:22.454") == dt
-
-    def testTranslatePythonObjectToString(self):
-        dt = datetime.datetime(2021, 12, 31, 11, 25, tzinfo=UtcTimezone())
-        translated = XDateTime.translate(dt, topython=False)
-        assert isinstance(translated, str)
-        assert translated == "2021-12-31T11:25:00+00:00"
-
-    @pytest.mark.parametrize("source", (
-        None,
-        object(),
-        _Dummy(),
-        datetime.time(22, 47, 9, 981),
-        datetime.date(2101, 1, 1)))
-    def testTranslatePythonObjectToString_failed(self, source):
-        assert XDateTime.translate(source, topython=False) is source
-
-
-class TestXTime:
-    """
-    Tests for the suds.xsd.sxbuiltin.XTime class.
-
-    Python object <--> string conversion details already tested in
-    TestDateTime.
-
-    """
-
-    def testTranslateEmptyStringToPythonObject(self):
-        assert XTime.translate("") == None
-
-    def testTranslateStringToPythonObject(self):
-        assert XTime.translate("10:30:22") == datetime.time(10, 30, 22)
-
-    def testTranslatePythonObjectToString(self):
-        time = datetime.time(16, 53, 12, tzinfo=FixedOffsetTimezone(4))
-        translated = XTime.translate(time, topython=False)
-        assert isinstance(translated, str)
-        assert translated == "16:53:12+04:00"
-
-    @pytest.mark.parametrize("source", (
-        None,
-        object(),
-        _Dummy(),
-        datetime.date(2101, 1, 1),
-        datetime.datetime(2101, 1, 1, 22, 47, 9, 981)))
-    def testTranslatePythonObjectToString_failed(self, source):
-        assert XTime.translate(source, topython=False) is source
diff --git a/tests/test_dependency_sort.py b/tests/test_dependency_sort.py
new file mode 100644
index 0000000..b48be3c
--- /dev/null
+++ b/tests/test_dependency_sort.py
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Dependency sort unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+import testutils
+if __name__ == "__main__":
+    testutils.run_using_pytest(globals())
+
+from suds.xsd.depsort import dependency_sort
+
+import pytest
+from six import iteritems
+
+import copy
+
+
+# some of the tests in this module make sense only with assertions enabled
+# (note though that pytest's assertion rewriting technique, as used by default
+# in recent pytest releases, will keep assertions enabled inside the used test
+# modules even when the underlying Python interpreter has been run using the -O
+# command-line option)
+try:
+    assert False
+except AssertionError:
+    assertions_enabled = True
+else:
+    assertions_enabled = False
+
+
+# shared test data
+
+# f --+-----+-----+
+# |   |     |     |
+# |   v     v     v
+# |   e --> d --> c --> b
+# |   |           |     |
+# +---+-----------+-----+--> a --> x
+_test_dependency_tree = {
+    "x": (),
+    "a": ("x",),
+    "b": ("a",),
+    "c": ("a", "b"),
+    "d": ("c",),
+    "e": ("d", "a"),
+    "f": ("e", "c", "d", "a")}
+
+
+def test_dependency_sort():
+    dependency_tree = _test_dependency_tree
+    result = dependency_sort(dependency_tree)
+    assert sorted(result) == sorted(iteritems(dependency_tree))
+    _assert_dependency_order((x[0] for x in result), dependency_tree)
+
+
+def test_dependency_sort_does_not_mutate_input():
+    dependency_tree = _test_dependency_tree
+
+    # save the original dependency tree structure information
+    expected_deps = {}
+    expected_deps_ids = {}
+    for x, y in iteritems(dependency_tree):
+        expected_deps[x] = copy.copy(y)
+        expected_deps_ids[id(x)] = id(y)
+
+    # run the dependency sort
+    dependency_sort(dependency_tree)
+
+    # verify that the dependency tree structure is unchanged
+    assert len(dependency_tree) == len(expected_deps)
+    for key, deps in iteritems(dependency_tree):
+        # same deps for each key
+        assert id(deps) == expected_deps_ids[id(key)]
+        # deps structure compare with the original copy
+        assert deps == expected_deps[key]
+        # explicit deps content id matching just in case the container's __eq__
+        # is not precise enough
+        _assert_same_content_set(deps, expected_deps[key])
+
+
+###############################################################################
+#
+# Test utilities.
+#
+###############################################################################
+
+def _assert_dependency_order(sequence, dependencies):
+    """
+    Assert that a sequence is ordered dependencies first.
+
+    The only way an earlier entry is allowed to have a later entry as its
+    dependency is if they are both part of the same dependency cycle.
+
+    """
+    sequence = list(sequence)
+    dependency_closure = _transitive_dependency_closure(dependencies)
+    for i, a in enumerate(sequence):
+        for b in sequence[i + 1:]:
+            a_dependent_on_b = b in dependency_closure[a]
+            b_dependent_on_a = a in dependency_closure[b]
+            assert b_dependent_on_a or not a_dependent_on_b
+
+
+def _assert_same_content_set(lhs, rhs):
+    """Assert that two iterables have the same content (order independent)."""
+    counter_lhs = _counter(lhs)
+    counter_rhs = _counter(rhs)
+    assert counter_lhs == counter_rhs
+
+
+def _counter(iterable):
+    """Return an {id: count} dictionary for all items from `iterable`."""
+    counter = {}
+    for x in iterable:
+        counter[id(x)] = counter.setdefault(id(x), 0) + 1
+    return counter
+
+
+def _transitive_dependency_closure(dependencies):
+    """
+    Returns a transitive dependency closure.
+
+    If target A is dependent on target B, and target B is in turn dependent on
+    target C, then target A is also implicitly dependent on target C. A
+    transitive dependency closure is an expanded dependency collection so that
+    in it all such implicit dependencies have been explicitly specified.
+
+    """
+    def clone(deps):
+        return dict((k, set(v)) for k, v in iteritems(deps))
+    closure = None
+    new = clone(dependencies)
+    while new != closure:
+        closure = clone(new)
+        for k, deps in iteritems(closure):
+            for dep in deps:
+                new[k] |= closure[dep]
+    return closure
+
+
+###############################################################################
+#
+# Test utility tests.
+#
+###############################################################################
+
+ at pytest.mark.skipif(not assertions_enabled, reason="assertions disabled")
+ at pytest.mark.parametrize("sequence, dependencies", (
+    (["x", "y"], {"x": ("y",), "y": ()}),
+    (["x", "y"], {"x": ("z",), "z": ("y",), "y": ()}),
+    (["y", "x", "z"], {"x": ("z",), "z": ("y",), "y": ()}),
+    (["z", "y", "x"], {"x": ("z",), "z": ("y",), "y": ()}),
+    # unrelated element groups
+    (["y", "a", "x", "b"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["x", "b", "y", "a"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["b", "x", "y", "a"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["a", "y", "b", "x"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["a", "b", "y", "x"], {"x": (), "y": ("x",), "a": (), "b": ("a",)})))
+def test_assert_dependency_order__invalid(sequence, dependencies):
+    pytest.raises(AssertionError, _assert_dependency_order, sequence,
+        dependencies)
+
+
+ at pytest.mark.parametrize("sequence, dependencies", (
+    (["y", "x"], {"x": ("y",), "y": ()}),
+    (["y", "x"], {"x": ("z",), "z": ("y",), "y": ()}),
+    (["y", "z", "x"], {"x": ("z",), "z": ("y",), "y": ()}),
+    # unrelated element groups
+    (["x", "y", "a", "b"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["x", "a", "y", "b"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["a", "x", "y", "b"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["a", "x", "b", "y"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    (["a", "b", "x", "y"], {"x": (), "y": ("x",), "a": (), "b": ("a",)}),
+    # dependency cycle
+    (["x", "y"], {"x": ("y",), "y": ("x",)}),
+    (["y", "x"], {"x": ("y",), "y": ("x",)}),
+    # elements not mentioned in the dependency tree
+    ([1], {}),
+    (["a"], {"x": ("y",), "y": ()})))
+def test_assert_dependency_order__valid(sequence, dependencies):
+    _assert_dependency_order(sequence, dependencies)
+
+
+ at pytest.mark.skipif(not assertions_enabled, reason="assertions disabled")
+ at pytest.mark.parametrize("lhs, rhs", (
+    # empty
+    #    ([1, 2.0, 6], [1, 2, 6]),
+    ((), (1,)),
+    ([2], []),
+    ([], (4, 2)),
+    ([], (x for x in [8, 4])),
+    ((x for x in [1, 1]), []),
+    # without duplicates
+    ([1, 2, 3], [1, 2, 4]),
+    ([1, 2, 3], [1, 2]),
+    ([1, 2, 3], [1, 4]),
+    ([0], [0.0]),
+    ([0], [0.0]),
+    # with duplicates
+    ([1, 1], [1]),
+    ((x for x in [1, 1]), [1]),
+    ([1, 1], [1, 2, 1]),
+    ([1, 1, 2, 2], [1, 2, 1]),
+    # different object ids
+    ([object()], [object()])))
+def test_assert_same_content_set__invalid(lhs, rhs):
+    pytest.raises(AssertionError, _assert_same_content_set, lhs, rhs)
+
+
+ at pytest.mark.parametrize("lhs, rhs", (
+    # empty
+    ((), ()),
+    ([], []),
+    ([], ()),
+    ([], (x for x in [])),
+    ((x for x in []), []),
+    # matching without duplicates
+    ([1, 2, 6], [1, 2, 6]),
+    ([1, 2, 6], [6, 2, 1]),
+    # matching with duplicates
+    ([1, 2, 2, 6], [6, 2, 1, 2]),
+    # matching object ids
+    ([_assert_same_content_set], [_assert_same_content_set])))
+def test_assert_same_content_set__valid(lhs, rhs):
+    _assert_same_content_set(lhs, rhs)
+
+
+def test_counter():
+    a = object()
+    b = object()
+    c = object()
+    d = object()
+    input = [a, b, b, c, c, d, a, a, a, d, b, b, b, b, b, a, d]
+    result = _counter(input)
+    assert len(result) == 4
+    assert result[id(a)] == input.count(a)
+    assert result[id(b)] == input.count(b)
+    assert result[id(c)] == input.count(c)
+    assert result[id(d)] == input.count(d)
+
+
+def test_counter__empty():
+    assert _counter([]) == {}
diff --git a/tests/test_document_store.py b/tests/test_document_store.py
index 64fcf10..60f2ba9 100644
--- a/tests/test_document_store.py
+++ b/tests/test_document_store.py
@@ -1,19 +1,18 @@
 # -*- coding: utf-8 -*-
 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
@@ -24,9 +23,8 @@ Implemented using the 'pytest' testing framework.
 """
 
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    import testutils
+    testutils.run_using_pytest(globals())
 
 import suds
 import suds.store
@@ -39,33 +37,33 @@ def test_accessing_DocumentStore_content():
     content2 = suds.byte_str("two")
     content1_1 = suds.byte_str("one one")
 
-    store = suds.store.DocumentStore({"1":content1})
+    store = suds.store.DocumentStore({"1": content1})
     assert len(store) == 2
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
 
-    store = suds.store.DocumentStore({"1":content1, "2":content2})
+    store = suds.store.DocumentStore({"1": content1, "2": content2})
     assert len(store) == 3
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
-    __test_open(store, "2", content2)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
+    _test_open(store, "2", content2)
 
     store = suds.store.DocumentStore(uno=content1, due=content2)
     assert len(store) == 3
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "uno", content1)
-    __test_open(store, "due", content2)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "uno", content1)
+    _test_open(store, "due", content2)
 
-    store = suds.store.DocumentStore({"1 1":content1_1})
+    store = suds.store.DocumentStore({"1 1": content1_1})
     assert len(store) == 2
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1 1", content1_1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1 1", content1_1)
 
-    store = suds.store.DocumentStore({"1":content1, "1 1":content1_1})
+    store = suds.store.DocumentStore({"1": content1, "1 1": content1_1})
     assert len(store) == 3
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
-    __test_open(store, "1 1", content1_1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
+    _test_open(store, "1 1", content1_1)
 
 
 def test_accessing_missing_DocumentStore_content():
@@ -80,7 +78,7 @@ def test_accessing_missing_DocumentStore_content():
 
 def test_default_DocumentStore_instance():
     assert len(suds.store.defaultDocumentStore) == 1
-    __test_default_DocumentStore_content(suds.store.defaultDocumentStore)
+    _test_default_DocumentStore_content(suds.store.defaultDocumentStore)
 
 
 def test_empty_DocumentStore_instance_is_not_shared():
@@ -95,43 +93,43 @@ def test_updating_DocumentStore_content():
 
     store = suds.store.DocumentStore()
     assert len(store) == 1
-    __test_default_DocumentStore_content(store)
+    _test_default_DocumentStore_content(store)
 
-    store.update({"1":content1})
+    store.update({"1": content1})
     assert len(store) == 2
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
 
-    store.update({"1":content1, "2":content2, "1 1":content1_1})
+    store.update({"1": content1, "2": content2, "1 1": content1_1})
     assert len(store) == 4
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
-    __test_open(store, "2", content2)
-    __test_open(store, "1 1", content1_1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
+    _test_open(store, "2", content2)
+    _test_open(store, "1 1", content1_1)
 
-    store.update({"2":content2, "1 1":content1_1})
+    store.update({"2": content2, "1 1": content1_1})
     assert len(store) == 4
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
-    __test_open(store, "2", content2)
-    __test_open(store, "1 1", content1_1)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
+    _test_open(store, "2", content2)
+    _test_open(store, "1 1", content1_1)
 
     store.update(uno=content1, due=content2)
     assert len(store) == 6
-    __test_default_DocumentStore_content(store)
-    __test_open(store, "1", content1)
-    __test_open(store, "2", content2)
-    __test_open(store, "1 1", content1_1)
-    __test_open(store, "uno", content1)
-    __test_open(store, "due", content2)
+    _test_default_DocumentStore_content(store)
+    _test_open(store, "1", content1)
+    _test_open(store, "2", content2)
+    _test_open(store, "1 1", content1_1)
+    _test_open(store, "uno", content1)
+    _test_open(store, "due", content2)
 
 
-def __test_default_DocumentStore_content(store):
-    __test_open(store, "schemas.xmlsoap.org/soap/encoding/",
+def _test_default_DocumentStore_content(store):
+    _test_open(store, "schemas.xmlsoap.org/soap/encoding/",
         suds.store.soap5_encoding_schema)
 
 
-def __test_open(store, location, expected_content):
+def _test_open(store, location, expected_content):
     assert store.open(location) is expected_content
     assert store.open("buga-wuga://%s" % location) is expected_content
     assert store.open("ftp://%s" % location) is expected_content
diff --git a/tests/test_input_parameters.py b/tests/test_input_parameters.py
index a4a6f01..f4ab7a7 100644
--- a/tests/test_input_parameters.py
+++ b/tests/test_input_parameters.py
@@ -35,13 +35,11 @@ operation's input parameters correctly.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
-import tests
 
 import pytest
 
@@ -246,7 +244,7 @@ class TestUnsupportedParameterDefinitions:
             else:
                 assert str(e) == expected_error_text
         finally:
-            del e
+            del e  # explicitly break circular reference chain in Python 3
 
     def init_function_params(self, params, **kwargs):
         """
@@ -265,8 +263,8 @@ class TestUnsupportedParameterDefinitions:
         """
         input = '<xsd:element name="Wrapper">%s</xsd:element>' % (params,)
         assert not hasattr(self, "service")
-        wsdl = tests.wsdl_input(input, "Wrapper", **kwargs)
-        client = tests.client_from_wsdl(wsdl, nosend=True)
+        wsdl = testutils.wsdl(input, input="Wrapper", **kwargs)
+        client = testutils.client_from_wsdl(wsdl, nosend=True)
         self.service = client.service
 
     @pytest.mark.parametrize("test_args_required", (
@@ -383,7 +381,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>""" % (part_name,))
-    client = tests.client_from_wsdl(wsdl, nosend=True)
+    client = testutils.client_from_wsdl(wsdl, nosend=True)
 
     # Collect references to required WSDL model content.
     method = client.wsdl.services[0].ports[0].methods["f"]
@@ -405,7 +403,7 @@ def test_explicitly_wrapped_parameter(part_name):
     """
     input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
     wsdl = _unwrappable_wsdl(part_name, input_schema)
-    client = tests.client_from_wsdl(wsdl, nosend=True, unwrap=False)
+    client = testutils.client_from_wsdl(wsdl, nosend=True, unwrap=False)
 
     # Collect references to required WSDL model content.
     method = client.wsdl.services[0].ports[0].methods["f"]
@@ -473,7 +471,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:service>
 </wsdl:definitions>""")
     wsdl = suds.byte_str("".join(wsdl))
-    client = tests.client_from_wsdl(wsdl, nosend=True)
+    client = testutils.client_from_wsdl(wsdl, nosend=True)
 
     # Collect references to required WSDL model content.
     method = client.wsdl.services[0].ports[0].methods["f"]
@@ -505,7 +503,7 @@ def test_unwrapped_parameter(xsd_type):
     """Test recognizing unwrapped web service operation input structures."""
     input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
     wsdl = _unwrappable_wsdl("part_name", input_schema)
-    client = tests.client_from_wsdl(wsdl, nosend=True)
+    client = testutils.client_from_wsdl(wsdl, nosend=True)
 
     # Collect references to required WSDL model content.
     method = client.wsdl.services[0].ports[0].methods["f"]
@@ -530,7 +528,7 @@ def test_unwrapped_parameter_part_name(part_name):
     """
     input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
     wsdl = _unwrappable_wsdl(part_name, input_schema)
-    client = tests.client_from_wsdl(wsdl, nosend=True)
+    client = testutils.client_from_wsdl(wsdl, nosend=True)
 
     # Collect references to required WSDL model content.
     method = client.wsdl.services[0].ports[0].methods["f"]
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 0000000..f3b57c5
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library plugin unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+import testutils
+if __name__ == "__main__":
+    testutils.run_using_pytest(globals())
+
+import pytest
+
+from suds.plugin import (DocumentContext, DocumentPlugin, InitContext,
+    InitPlugin, MessageContext, MessagePlugin, Plugin, PluginContainer,
+    PluginDomain)
+
+
+class MyDocumentPlugin(DocumentPlugin):
+    """Local 'document' plugin class used in the tests in this module."""
+    pass
+
+
+class MyInitPlugin(InitPlugin):
+    """Local 'init' plugin class used in the tests in this module."""
+    pass
+
+
+class MyMessagePlugin(MessagePlugin):
+    """Local 'message' plugin class used in the tests in this module."""
+    pass
+
+
+ at pytest.mark.parametrize("domain, expected_context", (
+    ("document", DocumentContext),
+    ("init", InitContext),
+    ("message", MessageContext)))
+def test_collecting_plugins_and_context_per_empty_domain(domain,
+        expected_context):
+    container = PluginContainer([])
+    result = getattr(container, domain)
+    assert result.__class__ is PluginDomain
+    assert result.ctx is expected_context
+    assert result.plugins == []
+
+
+ at pytest.mark.parametrize("domain, plugin_class", (
+    ("document", DocumentPlugin),
+    ("init", InitPlugin),
+    ("message", MessagePlugin)))
+def test_collecting_plugins_per_domain(domain, plugin_class):
+    plugins = [
+        MyDocumentPlugin(),
+        MyDocumentPlugin(),
+        MyMessagePlugin(),
+        MyDocumentPlugin(),
+        MyInitPlugin(),
+        MyInitPlugin(),
+        MyMessagePlugin(),
+        InitPlugin(),
+        MyMessagePlugin(),
+        MyMessagePlugin(),
+        None,
+        MessagePlugin(),
+        DocumentPlugin(),
+        MyMessagePlugin(),
+        MyDocumentPlugin(),
+        InitPlugin(),
+        InitPlugin(),
+        MyInitPlugin(),
+        MyInitPlugin(),
+        None,
+        MyDocumentPlugin(),
+        DocumentPlugin(),
+        MessagePlugin(),
+        DocumentPlugin(),
+        MessagePlugin(),
+        DocumentPlugin(),
+        InitPlugin(),
+        MessagePlugin(),
+        object(),
+        DocumentPlugin(),
+        MessagePlugin(),
+        object(),
+        InitPlugin(),
+        Plugin(),
+        Plugin(),
+        MyInitPlugin()]
+    container = PluginContainer(plugins)
+    expected_plugins = [p for p in plugins if isinstance(p, plugin_class)]
+    result = getattr(container, domain).plugins
+    assert result == expected_plugins
+
+
+def test_exception_passing():
+    class FailingPluginException(Exception):
+        pass
+
+    class FailingPlugin(MessagePlugin):
+        def marshalled(self, context):
+            raise FailingPluginException
+
+    container = PluginContainer([FailingPlugin()])
+    pytest.raises(FailingPluginException, container.message.marshalled)
+
+
+def test_invalid_plugin_domain():
+    container = PluginContainer([])
+    domain = "invalid_domain_name"
+    e = pytest.raises(Exception, getattr, container, domain)
+    try:
+        e = e.value
+        assert e.__class__ is Exception
+        assert str(e) == "plugin domain (%s), invalid" % (domain,)
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
diff --git a/tests/test_reader.py b/tests/test_reader.py
new file mode 100644
index 0000000..da6f489
--- /dev/null
+++ b/tests/test_reader.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+suds.reader module unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+import testutils
+if __name__ == "__main__":
+    testutils.run_using_pytest(globals())
+
+import suds
+import suds.options
+import suds.reader
+
+
+class TestCacheItemNameMangling:
+    """Tests suds.reader.Reader classes' cache item name mangling."""
+
+    def test_different(self):
+        test_item_name1 = "oh my god"
+        test_item_name2 = "ha ha ha"
+        test_item_suffix = "that's some funky sh*t"
+        reader = suds.reader.Reader(suds.options.Options())
+        mangled1 = reader.mangle(test_item_name1, test_item_suffix)
+        mangled2 = reader.mangle(test_item_name2, test_item_suffix)
+        assert mangled1 != mangled2
+
+    def test_inter_processes_persistence(self, tmpdir):
+        """
+        Same cache item names must be mangled the same in different processes.
+
+        This is a regression test against using a built-in Python hash()
+        function internally since that function may be seeded by a process
+        specific random seed. This Python interpreter behaviour has been
+        enabled by default since Python 3.3 and may be explicitly enabled on
+        earlier Python interpreter versions as well.
+
+        """
+        test_item_name = "test string"
+        test_item_suffix = "test suffix"
+        reader = suds.reader.Reader(suds.options.Options())
+        expected = reader.mangle(test_item_name, test_item_suffix)
+        test_file = tmpdir.join("test_mangle.py")
+        test_file.write("""
+import suds.options
+import suds.reader
+reader = suds.reader.Reader(suds.options.Options())
+mangled = reader.mangle("%(test_item_name)s", "%(test_item_suffix)s")
+assert mangled == '%(expected)s'
+""" % {"expected": expected,
+    "test_item_name": test_item_name,
+    "test_item_suffix": test_item_suffix})
+        testutils.run_test_process(test_file)
+
+    def test_repeatable__different_readers(self):
+        test_item_name = "R2D2"
+        test_item_suffix = "C3P0"
+        reader1 = suds.reader.Reader(suds.options.Options())
+        reader2 = suds.reader.Reader(suds.options.Options())
+        mangled1 = reader1.mangle(test_item_name, test_item_suffix)
+        mangled2 = reader2.mangle(test_item_name, test_item_suffix)
+        assert mangled1 == mangled2
+
+    def test_repeatable__same_reader(self):
+        test_item_name = "han solo"
+        test_item_suffix = "chewbacca"
+        reader = suds.reader.Reader(suds.options.Options())
+        mangled1 = reader.mangle(test_item_name, test_item_suffix)
+        mangled2 = reader.mangle(test_item_name, test_item_suffix)
+        assert mangled1 == mangled2
+
+    def test_suffix(self):
+        test_item_name = "and a one! and a two! and a one - two - three!"
+        test_item_suffix = "pimpl"
+        reader = suds.reader.Reader(suds.options.Options())
+        mangled = reader.mangle(test_item_name, test_item_suffix)
+        assert mangled.endswith(test_item_suffix)
diff --git a/tests/test_reply_handling.py b/tests/test_reply_handling.py
index 0a53724..f8a8a5f 100644
--- a/tests/test_reply_handling.py
+++ b/tests/test_reply_handling.py
@@ -1,19 +1,18 @@
 # -*- coding: utf-8 -*-
 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
@@ -23,223 +22,129 @@ Implemented using the 'pytest' testing framework.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
-import tests
 
 import pytest
+from six import itervalues, next, u
+from six.moves import http_client
 
-import httplib
-import re
 import xml.sax
 
 
 def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_with_faults():
-    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
-    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
-        "status":s})
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=True)
+    def f(reply, status):
+        inject = {"reply": suds.byte_str(reply), "status": status}
+        return client.service.f(__inject=inject)
     assert f("", None) is None
-    pytest.raises(Exception, f, "", httplib.INTERNAL_SERVER_ERROR)
-    assert f("", httplib.ACCEPTED) is None
-    assert f("", httplib.NO_CONTENT) is None
-    assert f("bla-bla", httplib.ACCEPTED) is None
-    assert f("bla-bla", httplib.NO_CONTENT) is None
+    pytest.raises(Exception, f, "", http_client.INTERNAL_SERVER_ERROR)
+    assert f("", http_client.ACCEPTED) is None
+    assert f("", http_client.NO_CONTENT) is None
+    assert f("bla-bla", http_client.ACCEPTED) is None
+    assert f("bla-bla", http_client.NO_CONTENT) is None
 
 
 def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_without_faults():
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
-    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
-        "status":s})
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
+    def f(reply, status):
+        inject = {"reply": suds.byte_str(reply), "status": status}
+        return client.service.f(__inject=inject)
     assert f("", None) is not None
-    assert f("", httplib.INTERNAL_SERVER_ERROR) is not None
-    assert f("", httplib.ACCEPTED) is None
-    assert f("", httplib.NO_CONTENT) is None
-    assert f("bla-bla", httplib.ACCEPTED) is None
-    assert f("bla-bla", httplib.NO_CONTENT) is None
+    assert f("", http_client.INTERNAL_SERVER_ERROR) is not None
+    assert f("", http_client.ACCEPTED) is None
+    assert f("", http_client.NO_CONTENT) is None
+    assert f("bla-bla", http_client.ACCEPTED) is None
+    assert f("bla-bla", http_client.NO_CONTENT) is None
 
 
 def test_badly_formed_reply_XML():
     for faults in (True, False):
-        client = tests.client_from_wsdl(_wsdl__simple, faults=faults)
+        client = testutils.client_from_wsdl(_wsdl__simple_f, faults=faults)
         pytest.raises(xml.sax.SAXParseException, client.service.f,
-            __inject={"reply":suds.byte_str("bad food")})
+            __inject={"reply": suds.byte_str("bad food")})
 
 
-# TODO: Update the current restriction type output parameter handling so such
+#TODO: Update the current restriction type output parameter handling so such
 # parameters get converted to the correct Python data type based on the
 # restriction's underlying data type.
 @pytest.mark.xfail
 def test_restriction_data_types():
-    client_unnamed = tests.client_from_wsdl(tests.wsdl_output("""\
+    client_unnamed = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Elemento">
         <xsd:simpleType>
           <xsd:restriction base="xsd:int">
-            <xsd:enumeration value="1" />
-            <xsd:enumeration value="3" />
-            <xsd:enumeration value="5" />
+            <xsd:enumeration value="1"/>
+            <xsd:enumeration value="3"/>
+            <xsd:enumeration value="5"/>
           </xsd:restriction>
         </xsd:simpleType>
-      </xsd:element>""", "Elemento"))
+      </xsd:element>""", output="Elemento"))
 
-    client_named = tests.client_from_wsdl(tests.wsdl_output("""\
+    client_named = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:simpleType name="MyType">
         <xsd:restriction base="xsd:int">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
-      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+      <xsd:element name="Elemento" type="ns:MyType"/>""", output="Elemento"))
 
-    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_output("""\
+    client_twice_restricted = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:simpleType name="MyTypeGeneric">
         <xsd:restriction base="xsd:int">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="2" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="4" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="2"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="4"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
       <xsd:simpleType name="MyType">
         <xsd:restriction base="ns:MyTypeGeneric">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
-      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+      <xsd:element name="Elemento" type="ns:MyType"/>""", output="Elemento"))
 
     for client in (client_unnamed, client_named, client_twice_restricted):
         response = client.service.f(__inject=dict(reply=suds.byte_str("""\
 <?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <Elemento xmlns="my-namespace">5</Elemento>
-  </env:Body>
-</env:Envelope>""")))
+  </Body>
+</Envelope>""")))
         assert response.__class__ is int
         assert response == 5
 
 
-def test_builtin_data_types(monkeypatch):
-    monkeypatch.delitem(locals(), "e", False)
-
-    integer_type_mapping = {
-        "byte":int,
-        "int":int,
-        "integer":int,
-        "long":long,
-        "negativeInteger":int,
-        "nonNegativeInteger":int,
-        "nonPositiveInteger":int,
-        "positiveInteger":int,
-        "short":int,
-        "unsignedByte":int,
-        "unsignedInt":int,
-        "unsignedLong":long,
-        "unsignedShort":int}
-    for tag, type in integer_type_mapping.items():
-        client = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="value" type="xsd:%s" />""" % tag, "value"))
-        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
-<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
-    <value xmlns="my-namespace">15</value>
-  </env:Body>
-</env:Envelope>""")))
-        assert response.__class__ is type
-        assert response == 15
-
-    boolean_mapping = {"0":False, "1":True, "false":False, "true":True}
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="value" type="xsd:boolean" />""", "value"))
-    for value, expected_value in boolean_mapping.items():
-        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
-<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
-    <value xmlns="my-namespace">%s</value>
-  </env:Body>
-</env:Envelope>""" % value)))
-        assert response.__class__ is bool
-        assert response == expected_value
-
-    # Suds implements no extra range checking.
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="value" type="xsd:byte" />""", "value"))
-    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
-<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
-    <value xmlns="my-namespace">1500</value>
-  </env:Body>
-</env:Envelope>""")))
-    assert response.__class__ is int
-    assert response == 1500
-
-    #   Suds raises raw Python exceptions when it fails to convert received
-    # response element data to its mapped Python integer data type, according
-    # to the used WSDL schema.
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="value" type="xsd:int" />""", "value"))
-    e = pytest.raises(ValueError, client.service.f, __inject=dict(
-        reply=suds.byte_str("""\
-<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
-    <value xmlns="my-namespace">Fifteen</value>
-  </env:Body>
-</env:Envelope>"""))).value
-    # ValueError instance received here has different string representations
-    # depending on the Python version used:
-    #   Python 2.4:
-    #     "invalid literal for int(): Fifteen"
-    #   Python 2.7.3, 3.2.3:
-    #     "invalid literal for int() with base 10: 'Fifteen'"
-    assert re.match("invalid literal for int\(\)( with base 10)?: ('?)Fifteen"
-        "\\2$", str(e))
-
-    # Suds returns invalid boolean values as None.
-    invalid_boolean_values = ("True", "", "False", "2", "Fedora", "Z", "-1")
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="value" type="xsd:boolean" />""", "value"))
-    for value in invalid_boolean_values:
-        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
-<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
-    <value xmlns="my-namespace">%s</value>
-  </env:Body>
-</env:Envelope>""" % value)))
-        assert response is None
-
-
 def test_disabling_automated_simple_interface_unwrapping():
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
+    client = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="Elemento" type="xsd:string" />
+            <xsd:element name="Elemento" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"), unwrap=False)
+      </xsd:element>""", output="Wrapper"), unwrap=False)
     assert not _isOutputWrapped(client, "f")
 
     response = client.service.f(__inject=dict(reply=suds.byte_str("""\
 <?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <Wrapper xmlns="my-namespace">
         <Elemento>La-di-da-da-da</Elemento>
     </Wrapper>
-  </env:Body>
-</env:Envelope>""")))
+  </Body>
+</Envelope>""")))
 
     assert response.__class__.__name__ == "Wrapper"
     assert len(response.__class__.__bases__) == 1
@@ -249,31 +154,35 @@ def test_disabling_automated_simple_interface_unwrapping():
 
 
 def test_empty_reply():
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
-    f = lambda status=None, description=None : client.service.f(__inject=dict(
-        reply=suds.byte_str(), status=status, description=description))
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
+    def f(status=None, description=None):
+        inject = dict(reply=suds.byte_str(), status=status,
+            description=description)
+        return client.service.f(__inject=inject)
     status, reason = f()
-    assert status == httplib.OK
+    assert status == http_client.OK
     assert reason is None
-    status, reason = f(httplib.OK)
-    assert status == httplib.OK
+    status, reason = f(http_client.OK)
+    assert status == http_client.OK
     assert reason is None
-    status, reason = f(httplib.INTERNAL_SERVER_ERROR)
-    assert status == httplib.INTERNAL_SERVER_ERROR
-    assert reason == 'injected reply'
-    status, reason = f(httplib.FORBIDDEN)
-    assert status == httplib.FORBIDDEN
-    assert reason == 'injected reply'
-    status, reason = f(httplib.FORBIDDEN, "kwack")
-    assert status == httplib.FORBIDDEN
-    assert reason == 'kwack'
+    status, reason = f(http_client.INTERNAL_SERVER_ERROR)
+    assert status == http_client.INTERNAL_SERVER_ERROR
+    assert reason == "injected reply"
+    status, reason = f(http_client.FORBIDDEN)
+    assert status == http_client.FORBIDDEN
+    assert reason == "injected reply"
+    status, reason = f(http_client.FORBIDDEN, "kwack")
+    assert status == http_client.FORBIDDEN
+    assert reason == "kwack"
 
 
 def test_fault_reply_with_unicode_faultstring(monkeypatch):
     monkeypatch.delitem(locals(), "e", False)
 
-    unicode_string = u"€ Jurko Gospodnetić ČĆŽŠĐčćžšđ"
-    fault_xml = suds.byte_str(u"""\
+    unicode_string = u("\u20AC Jurko Gospodneti\u0107 "
+        "\u010C\u0106\u017D\u0160\u0110"
+        "\u010D\u0107\u017E\u0161\u0111")
+    fault_xml = suds.byte_str(u("""\
 <?xml version="1.0"?>
 <env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
   <env:Body>
@@ -283,18 +192,21 @@ def test_fault_reply_with_unicode_faultstring(monkeypatch):
     </env:Fault>
   </env:Body>
 </env:Envelope>
-""" % unicode_string)
+""") % (unicode_string,))
 
-    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
-    inject = dict(reply=fault_xml, status=httplib.INTERNAL_SERVER_ERROR)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=True)
+    inject = dict(reply=fault_xml, status=http_client.INTERNAL_SERVER_ERROR)
     e = pytest.raises(suds.WebFault, client.service.f, __inject=inject).value
-    assert e.fault.faultstring == unicode_string
-    assert e.document.__class__ is suds.sax.document.Document
+    try:
+        assert e.fault.faultstring == unicode_string
+        assert e.document.__class__ is suds.sax.document.Document
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
     status, fault = client.service.f(__inject=dict(reply=fault_xml,
-        status=httplib.INTERNAL_SERVER_ERROR))
-    assert status == httplib.INTERNAL_SERVER_ERROR
+        status=http_client.INTERNAL_SERVER_ERROR))
+    assert status == http_client.INTERNAL_SERVER_ERROR
     assert fault.faultstring == unicode_string
 
 
@@ -315,14 +227,17 @@ def test_invalid_fault_namespace(monkeypatch):
   </env:Body>
 </env:Envelope>
 """)
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
-    inject = dict(reply=fault_xml, status=httplib.OK)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
+    inject = dict(reply=fault_xml, status=http_client.OK)
     e = pytest.raises(Exception, client.service.f, __inject=inject).value
-    assert e.__class__ is Exception
-    assert str(e) == "<faultcode/> not mapped to message part"
-
-    for http_status in (httplib.INTERNAL_SERVER_ERROR,
-        httplib.PAYMENT_REQUIRED):
+    try:
+        assert e.__class__ is Exception
+        assert str(e) == "<faultcode/> not mapped to message part"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
+
+    for http_status in (http_client.INTERNAL_SERVER_ERROR,
+        http_client.PAYMENT_REQUIRED):
         status, reason = client.service.f(__inject=dict(reply=fault_xml,
             status=http_status, description="trla baba lan"))
         assert status == http_status
@@ -335,177 +250,190 @@ def test_missing_wrapper_response():
     interpreting received SOAP Response XML.
 
     """
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
+    client = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="fResponse" type="xsd:string" />
+            <xsd:element name="fResponse" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", output="Wrapper"))
     assert _isOutputWrapped(client, "f")
 
     response_with_missing_wrapper = client.service.f(__inject=dict(
         reply=suds.byte_str("""<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <fResponse xmlns="my-namespace">Anything</fResponse>
-  </env:Body>
-</env:Envelope>""")))
+  </Body>
+</Envelope>""")))
     assert response_with_missing_wrapper is None
 
 
 def test_reply_error_with_detail_with_fault(monkeypatch):
     monkeypatch.delitem(locals(), "e", False)
 
-    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=True)
 
-    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+    for http_status in (http_client.OK, http_client.INTERNAL_SERVER_ERROR):
         inject = dict(reply=_fault_reply__with_detail, status=http_status)
         e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
-        e = e.value
-        _test_fault(e.fault, True)
-        assert e.document.__class__ is suds.sax.document.Document
-        assert str(e) == "Server raised fault: 'Dummy error.'"
-
-    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
-        description="quack-quack")
+        try:
+            e = e.value
+            _test_fault(e.fault, True)
+            assert e.document.__class__ is suds.sax.document.Document
+            assert str(e) == "Server raised fault: 'Dummy error.'"
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    inject = dict(reply=_fault_reply__with_detail,
+        status=http_client.BAD_REQUEST, description="quack-quack")
     e = pytest.raises(Exception, client.service.f, __inject=inject).value
-    assert e.__class__ is Exception
-    assert e.args[0][0] == httplib.BAD_REQUEST
-    assert e.args[0][1] == "quack-quack"
+    try:
+        assert e.__class__ is Exception
+        assert e.args[0][0] == http_client.BAD_REQUEST
+        assert e.args[0][1] == "quack-quack"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
 
 def test_reply_error_with_detail_without_fault():
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
 
-    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+    for http_status in (http_client.OK, http_client.INTERNAL_SERVER_ERROR):
         status, fault = client.service.f(__inject=dict(
             reply=_fault_reply__with_detail, status=http_status))
-        assert status == httplib.INTERNAL_SERVER_ERROR
+        assert status == http_client.INTERNAL_SERVER_ERROR
         _test_fault(fault, True)
 
     status, fault = client.service.f(__inject=dict(
-        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST))
-    assert status == httplib.BAD_REQUEST
+        reply=_fault_reply__with_detail, status=http_client.BAD_REQUEST))
+    assert status == http_client.BAD_REQUEST
     assert fault == "injected reply"
 
     status, fault = client.service.f(__inject=dict(
-        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
+        reply=_fault_reply__with_detail, status=http_client.BAD_REQUEST,
         description="haleluja"))
-    assert status == httplib.BAD_REQUEST
+    assert status == http_client.BAD_REQUEST
     assert fault == "haleluja"
 
 
 def test_reply_error_without_detail_with_fault(monkeypatch):
     monkeypatch.delitem(locals(), "e", False)
 
-    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=True)
 
-    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+    for http_status in (http_client.OK, http_client.INTERNAL_SERVER_ERROR):
         inject = dict(reply=_fault_reply__without_detail, status=http_status)
         e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
-        e = e.value
-        _test_fault(e.fault, False)
-        assert e.document.__class__ is suds.sax.document.Document
-        assert str(e) == "Server raised fault: 'Dummy error.'"
-
-    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
-        description="quack-quack")
+        try:
+            e = e.value
+            _test_fault(e.fault, False)
+            assert e.document.__class__ is suds.sax.document.Document
+            assert str(e) == "Server raised fault: 'Dummy error.'"
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    inject = dict(reply=_fault_reply__with_detail,
+        status=http_client.BAD_REQUEST, description="quack-quack")
     e = pytest.raises(Exception, client.service.f, __inject=inject).value
-    assert e.__class__ is Exception
-    assert e.args[0][0] == httplib.BAD_REQUEST
-    assert e.args[0][1] == "quack-quack"
+    try:
+        assert e.__class__ is Exception
+        assert e.args[0][0] == http_client.BAD_REQUEST
+        assert e.args[0][1] == "quack-quack"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
 
 def test_reply_error_without_detail_without_fault():
-    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    client = testutils.client_from_wsdl(_wsdl__simple_f, faults=False)
 
-    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+    for http_status in (http_client.OK, http_client.INTERNAL_SERVER_ERROR):
         status, fault = client.service.f(__inject=dict(
             reply=_fault_reply__without_detail, status=http_status))
-        assert status == httplib.INTERNAL_SERVER_ERROR
+        assert status == http_client.INTERNAL_SERVER_ERROR
         _test_fault(fault, False)
 
     status, fault = client.service.f(__inject=dict(
-        reply=_fault_reply__without_detail, status=httplib.BAD_REQUEST,
+        reply=_fault_reply__without_detail, status=http_client.BAD_REQUEST,
         description="kung-fu-fui"))
-    assert status == httplib.BAD_REQUEST
+    assert status == http_client.BAD_REQUEST
     assert fault == "kung-fu-fui"
 
 
 def test_simple_bare_and_wrapped_output():
     # Prepare web service proxies.
-    client_bare = tests.client_from_wsdl(tests.wsdl_output("""\
-      <xsd:element name="fResponse" type="xsd:string" />""", "fResponse"))
-    client_wrapped = tests.client_from_wsdl(tests.wsdl_output("""\
+    client_bare = testutils.client_from_wsdl(testutils.wsdl("""\
+      <xsd:element name="fResponse" type="xsd:string"/>""",
+        output="fResponse"))
+    client_wrapped = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="fResponse" type="xsd:string" />
+            <xsd:element name="fResponse" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", output="Wrapper"))
 
-    #   Make sure suds library inteprets our WSDL definitions as wrapped or
-    # bare output interfaces as expected.
+    # Make sure suds library inteprets our WSDL definitions as wrapped or bare
+    # output interfaces as expected.
     assert not _isOutputWrapped(client_bare, "f")
     assert _isOutputWrapped(client_wrapped, "f")
 
-    #   Both bare & wrapped single parameter output web service operation
-    # results get presented the same way even though the wrapped one actually
-    # has an extra wrapper element around its received output data.
+    # Both bare & wrapped single parameter output web service operation results
+    # get presented the same way even though the wrapped one actually has an
+    # extra wrapper element around its received output data.
     data = "The meaning of life."
-    get_response = lambda client, x : client.service.f(__inject=dict(
-        reply=suds.byte_str(x)))
+    def get_response(client, x):
+        return client.service.f(__inject=dict(reply=suds.byte_str(x)))
 
     response_bare = get_response(client_bare, """<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <fResponse xmlns="my-namespace">%s</fResponse>
-  </env:Body>
-</env:Envelope>""" % data)
+  </Body>
+</Envelope>""" % (data,))
     assert response_bare.__class__ is suds.sax.text.Text
     assert response_bare == data
 
     response_wrapped = get_response(client_wrapped, """<?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <Wrapper xmlns="my-namespace">
       <fResponse>%s</fResponse>
     </Wrapper>
-  </env:Body>
-</env:Envelope>""" % data)
+  </Body>
+</Envelope>""" % (data,))
     assert response_wrapped.__class__ is suds.sax.text.Text
     assert response_wrapped == data
 
 
 def test_wrapped_sequence_output():
-    client = tests.client_from_wsdl(tests.wsdl_output("""\
+    client = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="result1" type="xsd:string" />
-            <xsd:element name="result2" type="xsd:string" />
-            <xsd:element name="result3" type="xsd:string" />
+            <xsd:element name="result1" type="xsd:string"/>
+            <xsd:element name="result2" type="xsd:string"/>
+            <xsd:element name="result3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", output="Wrapper"))
     assert _isOutputWrapped(client, "f")
 
     response = client.service.f(__inject=dict(reply=suds.byte_str("""\
 <?xml version="1.0"?>
-<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
-  <env:Body>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
     <Wrapper xmlns="my-namespace">
         <result1>Uno</result1>
         <result2>Due</result2>
         <result3>Tre</result3>
     </Wrapper>
-  </env:Body>
-</env:Envelope>""")))
+  </Body>
+</Envelope>""")))
 
-    #   Composite replies always get unmarshalled as a dynamically constructed
+    # Composite replies always get unmarshalled as a dynamically constructed
     # class named 'reply'.
     assert len(response.__class__.__bases__) == 1
     assert response.__class__.__name__ == "reply"
@@ -521,7 +449,7 @@ def test_wrapped_sequence_output():
     assert response.result3.__class__ is suds.sax.text.Text
 
 
-def _attibutes(object):
+def _attributes(object):
     result = set()
     for x in object:
         result.add(x[0])
@@ -530,7 +458,8 @@ def _attibutes(object):
 
 def _isOutputWrapped(client, method_name):
     assert len(client.wsdl.bindings) == 1
-    operation = client.wsdl.bindings.values()[0].operations[method_name]
+    binding = next(itervalues(client.wsdl.bindings))
+    operation = binding.operations[method_name]
     return operation.soap.output.body.wrapped
 
 
@@ -543,8 +472,8 @@ def _test_fault(fault, has_detail):
     expected_attributes = set(("faultcode", "faultstring"))
     if has_detail:
         expected_attributes.add("detail")
-    assert _attibutes(fault) == expected_attributes
-    assert not has_detail or _attibutes(fault.detail) == set(("errorcode",))
+    assert _attributes(fault) == expected_attributes
+    assert not has_detail or _attributes(fault.detail) == set(("errorcode",))
 
 
 _fault_reply__with_detail = suds.byte_str("""\
@@ -574,12 +503,12 @@ _fault_reply__without_detail = suds.byte_str("""\
 </env:Envelope>
 """)
 
-_wsdl__simple = tests.wsdl_output("""\
+_wsdl__simple_f = testutils.wsdl("""\
       <xsd:element name="fResponse">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="output_i" type="xsd:integer" />
-            <xsd:element name="output_s" type="xsd:string" />
+            <xsd:element name="output_i" type="xsd:integer"/>
+            <xsd:element name="output_s" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "fResponse")
+      </xsd:element>""", output="fResponse", operation_name="f")
diff --git a/tests/test_request_construction.py b/tests/test_request_construction.py
index 94894f7..4ce9e8a 100644
--- a/tests/test_request_construction.py
+++ b/tests/test_request_construction.py
@@ -28,24 +28,24 @@ then passing that wrapper object instead.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
 import suds.store
-import tests
+from testutils.compare_sax import CompareSAX
 
 import pytest
+from six import iterkeys, itervalues, next, u
 
 
-# TODO: Update the current restriction type output parameter handling so such
+#TODO: Update the current restriction type output parameter handling so such
 # parameters get converted to the correct Python data type based on the
 # restriction's underlying data type.
 @pytest.mark.xfail
 def test_bare_input_restriction_types():
-    client_unnamed = tests.client_from_wsdl(tests.wsdl_input("""\
+    client_unnamed = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Elemento">
         <xsd:simpleType>
           <xsd:restriction base="xsd:string">
@@ -54,9 +54,9 @@ def test_bare_input_restriction_types():
             <xsd:enumeration value="gamma"/>
           </xsd:restriction>
         </xsd:simpleType>
-      </xsd:element>""", "Elemento"))
+      </xsd:element>""", input="Elemento", operation_name="f"))
 
-    client_named = tests.client_from_wsdl(tests.wsdl_input("""\
+    client_named = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:simpleType name="MyType">
         <xsd:restriction base="xsd:string">
           <xsd:enumeration value="alfa"/>
@@ -64,10 +64,11 @@ def test_bare_input_restriction_types():
           <xsd:enumeration value="gamma"/>
         </xsd:restriction>
       </xsd:simpleType>
-      <xsd:element name="Elemento" type="ns:MyType"/>""", "Elemento"))
+      <xsd:element name="Elemento" type="ns:MyType"/>""", input="Elemento",
+        operation_name="f"))
 
-    assert not _isInputWrapped(client_unnamed, "f")
-    assert not _isInputWrapped(client_named, "f")
+    assert not _is_input_wrapped(client_unnamed, "f")
+    assert not _is_input_wrapped(client_named, "f")
 
 
 def parametrize_single_element_input_test(param_names, param_values):
@@ -101,12 +102,12 @@ def parametrize_single_element_input_test(param_names, param_values):
     ("xsd", "external_element_name", "args", "request_body"), (
     # Bare non-optional element.
     ('<xsd:element name="a" type="xsd:integer"/>', "a",
-        ([], "<ns0:a/>"),
-        ([5], "<ns0:a>5</ns0:a>")),
+        ([], "<a/>"),
+        ([5], "<a>5</a>")),
     # Bare optional element.
     ('<xsd:element name="a" type="xsd:integer" minOccurs="0"/>', "a",
         ([], ""),
-        ([5], "<ns0:a>5</ns0:a>")),
+        ([5], "<a>5</a>")),
     # Choice with a non-empty sub-sequence.
     ("""\
       <xsd:element name="Wrapper">
@@ -120,15 +121,15 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>",
+        ([], "<Wrapper><a/></Wrapper>",
             "non-optional choice handling buggy"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
-        ([None, 1], "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>",
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
+        ([None, 1], "<Wrapper><b1>1</b1><b2/></Wrapper>",
             "non-optional choice handling buggy"),
         ([None, 1, 2],
-            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2>2</ns0:b2></ns0:Wrapper>"),
+            "<Wrapper><b1>1</b1><b2>2</b2></Wrapper>"),
         ([None, None, 1],
-            "<ns0:Wrapper><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>",
+            "<Wrapper><b1/><b2>1</b2></Wrapper>",
             "non-optional choice handling buggy")),
     # Choice with a non-optional element.
     ("""\
@@ -139,9 +140,9 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>",
+        ([], "<Wrapper><a/></Wrapper>",
             "non-optional choice handling buggy"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+        ([5], "<Wrapper><a>5</a></Wrapper>")),
     # Choice with an optional element.
     ("""\
       <xsd:element name="Wrapper">
@@ -151,8 +152,8 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>")),
     # Choices with multiple elements, at least one of which is optional.
     ("""\
       <xsd:element name="Wrapper">
@@ -163,9 +164,9 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
-        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
+        ([None, 5], "<Wrapper><b>5</b></Wrapper>")),
     ("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
@@ -175,9 +176,9 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
-        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
+        ([None, 5], "<Wrapper><b>5</b></Wrapper>")),
     ("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
@@ -187,9 +188,9 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
-        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
+        ([None, 5], "<Wrapper><b>5</b></Wrapper>")),
     # Choice with multiple non-empty sub-sequences.
     ("""\
       <xsd:element name="Wrapper">
@@ -206,24 +207,24 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:a1/><ns0:a2/></ns0:Wrapper>",
+        ([], "<Wrapper><a1/><a2/></Wrapper>",
             "non-optional choice handling buggy"),
-        ([5], "<ns0:Wrapper><ns0:a1>5</ns0:a1><ns0:a2/></ns0:Wrapper>",
+        ([5], "<Wrapper><a1>5</a1><a2/></Wrapper>",
             "non-optional choice handling buggy"),
         ([5, 9], """\
-          <ns0:Wrapper>
-            <ns0:a1>5</ns0:a1>
-            <ns0:a2>9</ns0:a2>
-          </ns0:Wrapper>"""),
-        ([None, 1], "<ns0:Wrapper><ns0:a1/><ns0:a2>1</ns0:a2></ns0:Wrapper>",
+          <Wrapper>
+            <a1>5</a1>
+            <a2>9</a2>
+          </Wrapper>"""),
+        ([None, 1], "<Wrapper><a1/><a2>1</a2></Wrapper>",
             "non-optional choice handling buggy"),
         ([None, None, 1],
-            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>",
+            "<Wrapper><b1>1</b1><b2/></Wrapper>",
             "non-optional choice handling buggy"),
         ([None, None, 1, 2],
-            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2>2</ns0:b2></ns0:Wrapper>"),
+            "<Wrapper><b1>1</b1><b2>2</b2></Wrapper>"),
         ([None, None, None, 1],
-            "<ns0:Wrapper><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>",
+            "<Wrapper><b1/><b2>1</b2></Wrapper>",
             "non-optional choice handling buggy")),
     # Empty choice.
     ("""\
@@ -232,7 +233,7 @@ def parametrize_single_element_input_test(param_names, param_values):
           <xsd:choice/>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>")),
+        ([], "<Wrapper/>")),
     # Empty sequence.
     ("""\
       <xsd:element name="Wrapper">
@@ -240,7 +241,7 @@ def parametrize_single_element_input_test(param_names, param_values):
           <xsd:sequence/>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>")),
+        ([], "<Wrapper/>")),
     # Optional choice.
     ("""\
       <xsd:element name="Wrapper">
@@ -251,18 +252,18 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>",
+        ([], "<Wrapper/>",
             # This test passes by accident - the following two bugs seem to
             # cancel each other out:
             #  - choice order indicators explicitly marked optional unsupported
             #  - not constructing correct input parameter values when using no
             #    input arguments for a choice
-            #"suds does not yet support minOccurs/maxOccurs attributes on "
-            #"all/choice/sequence order indicators"
+            #"suds does not yet support minOccurs/maxOccurs attributes on all/"
+            #"choice/sequence order indicators"),
             ),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
         ([None, 1],
-            "<ns0:Wrapper><ns0:b>1</ns0:b></ns0:Wrapper>")),
+            "<Wrapper><b>1</b></Wrapper>")),
     # Optional sequence.
     ("""\
       <xsd:element name="Wrapper">
@@ -273,17 +274,17 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>",
+        ([], "<Wrapper/>",
             "suds does not yet support minOccurs/maxOccurs attributes on all/"
             "choice/sequence order indicators"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b/></ns0:Wrapper>"),
+        ([5], "<Wrapper><a>5</a><b/></Wrapper>"),
         ([None, 1],
-            "<ns0:Wrapper><ns0:a/><ns0:b>1</ns0:b></ns0:Wrapper>"),
+            "<Wrapper><a/><b>1</b></Wrapper>"),
         ([1, 2], """\
-            <ns0:Wrapper>
-              <ns0:a>1</ns0:a>
-              <ns0:b>2</ns0:b>
-            </ns0:Wrapper>""")),
+            <Wrapper>
+              <a>1</a>
+              <b>2</b>
+            </Wrapper>""")),
     # Sequence with a non-empty sub-sequence.
     ("""\
       <xsd:element name="Wrapper">
@@ -297,18 +298,18 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:a/><ns0:b1/><ns0:b2/></ns0:Wrapper>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b1/><ns0:b2/></ns0:Wrapper>"),
+        ([], "<Wrapper><a/><b1/><b2/></Wrapper>"),
+        ([5], "<Wrapper><a>5</a><b1/><b2/></Wrapper>"),
         ([None, 1],
-            "<ns0:Wrapper><ns0:a/><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>"),
+            "<Wrapper><a/><b1>1</b1><b2/></Wrapper>"),
         ([None, 1, 2], """\
-            <ns0:Wrapper>
-              <ns0:a/>
-              <ns0:b1>1</ns0:b1>
-              <ns0:b2>2</ns0:b2>
-            </ns0:Wrapper>"""),
+            <Wrapper>
+              <a/>
+              <b1>1</b1>
+              <b2>2</b2>
+            </Wrapper>"""),
         ([None, None, 1],
-            "<ns0:Wrapper><ns0:a/><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>")),
+            "<Wrapper><a/><b1/><b2>1</b2></Wrapper>")),
     # Sequence with a non-optional element.
     ("""\
       <xsd:element name="Wrapper">
@@ -318,8 +319,8 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+        ([], "<Wrapper><a/></Wrapper>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>")),
     # Sequence with an optional element.
     ("""\
       <xsd:element name="Wrapper">
@@ -329,8 +330,8 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>")),
     # Sequence with multiple consecutive choices.
     ("""\
       <xsd:element name="Wrapper">
@@ -347,19 +348,19 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper><ns0:aString1/></ns0:Wrapper>",
+        ([], "<Wrapper><aString1/></Wrapper>",
             "non-optional choice handling buggy"),
-        ([5], "<ns0:Wrapper><ns0:aString1>5</ns0:aString1></ns0:Wrapper>"),
+        ([5], "<Wrapper><aString1>5</aString1></Wrapper>"),
         ([None, 1, 2], """\
-            <ns0:Wrapper>
-              <ns0:anInt1>1</ns0:anInt1>
-              <ns0:aString2>2</ns0:aString2>
-            </ns0:Wrapper>"""),
+            <Wrapper>
+              <anInt1>1</anInt1>
+              <aString2>2</aString2>
+            </Wrapper>"""),
         ([None, 1, None, 2], """\
-            <ns0:Wrapper>
-              <ns0:anInt1>1</ns0:anInt1>
-              <ns0:anInt2>2</ns0:anInt2>
-            </ns0:Wrapper>""")),
+            <Wrapper>
+              <anInt1>1</anInt1>
+              <anInt2>2</anInt2>
+            </Wrapper>""")),
     # Sequence with multiple optional elements.
     ("""\
       <xsd:element name="Wrapper">
@@ -370,58 +371,62 @@ def parametrize_single_element_input_test(param_names, param_values):
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper",
-        ([], "<ns0:Wrapper/>"),
-        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
-        ([None, 1], "<ns0:Wrapper><ns0:b>1</ns0:b></ns0:Wrapper>"),
+        ([], "<Wrapper/>"),
+        ([5], "<Wrapper><a>5</a></Wrapper>"),
+        ([None, 1], "<Wrapper><b>1</b></Wrapper>"),
         ([5, 1],
-            "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b>1</ns0:b></ns0:Wrapper>")),
+            "<Wrapper><a>5</a><b>1</b></Wrapper>")),
     ))
 def test_document_literal_request_for_single_element_input(xsd,
         external_element_name, args, request_body):
-    wsdl = tests.wsdl_input(xsd, external_element_name)
-    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
-
-    assert _compare_request(client.service.f(*args), """\
+    wsdl = testutils.wsdl(xsd, input=external_element_name,
+        xsd_target_namespace="dr. Doolittle", operation_name="f")
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    _assert_request_content(client.service.f(*args), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>%s</ns1:Body>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+  <SOAP-ENV:Header/>
+  <SOAP-ENV:Body xmlns="dr. Doolittle">%s</SOAP-ENV:Body>
 </SOAP-ENV:Envelope>""" % (request_body,))
 
 
 def test_disabling_automated_simple_interface_unwrapping():
-    client = tests.client_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "woof"
+    wsdl = testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
             <xsd:element name="Elemento" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"), nosend=True, prettyxml=True, unwrap=False)
-    assert not _isInputWrapped(client, "f")
-    wrapper = client.factory.create("Wrapper")
-    wrapper.Elemento = "Wonderwall"
-    assert _compare_request(client.service.f(Wrapper=wrapper), """\
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace)
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True,
+        unwrap=False)
+    assert not _is_input_wrapped(client, "f")
+    element_data = "Wonderwall"
+    wrapper = client.factory.create("my_xsd:Wrapper")
+    wrapper.Elemento = element_data
+    _assert_request_content(client.service.f(Wrapper=wrapper), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:Elemento>Wonderwall</ns0:Elemento>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <Elemento>%s</Elemento>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace, element_data))
 
 
 def test_element_references_to_different_namespaces():
     wsdl = suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
-<wsdl:definitions
+<wsdl:definitions targetNamespace="first-namespace"
     xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
-    xmlns:tns="first-namespace"
-    targetNamespace="first-namespace">
+    xmlns:tns="first-namespace">
 
   <wsdl:types>
     <xsd:schema
@@ -466,35 +471,34 @@ def test_element_references_to_different_namespaces():
       <soap:address location="BoogaWooga"/>
     </wsdl:port>
   </wsdl:service>
-</wsdl:definitions>
-""")
+</wsdl:definitions>""")
 
     external_schema = suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <schema
-    xmlns="http://www.w3.org/2001/XMLSchema"
-    targetNamespace="second-namespace">
+    targetNamespace="second-namespace"
+    elementFormDefault="qualified"
+    xmlns="http://www.w3.org/2001/XMLSchema">
   <element name="external" type="string"/>
-</schema>
-""")
+</schema>""")
 
     store = suds.store.DocumentStore(external_schema=external_schema,
         wsdl=wsdl)
     client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
         nosend=True, prettyxml=True)
-    assert _compare_request(client.service.f(local="--L--",
+    _assert_request_content(client.service.f(local="--L--",
         local_referenced="--LR--", external="--E--"), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns1="first-namespace" xmlns:ns2="second-namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <SOAP-ENV:Body>
-      <ns1:fRequest>
-         <ns1:local>--L--</ns1:local>
-         <ns1:local_referenced>--LR--</ns1:local_referenced>
-         <ns2:external>--E--</ns2:external>
-      </ns1:fRequest>
-   </SOAP-ENV:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body xmlns:ns1="first-namespace" xmlns:ns2="second-namespace">
+    <ns1:fRequest>
+      <ns1:local>--L--</ns1:local>
+      <ns1:local_referenced>--LR--</ns1:local_referenced>
+      <ns2:external>--E--</ns2:external>
+    </ns1:fRequest>
+  </Body>
+</Envelope>""")
 
 
 def test_invalid_input_parameter_type_handling():
@@ -506,7 +510,8 @@ def test_invalid_input_parameter_type_handling():
     report this error.
 
     """
-    client = tests.client_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "1234567890"
+    wsdl = testutils.wsdl("""\
       <xsd:complexType name="Freakazoid">
         <xsd:sequence>
           <xsd:element name="freak1" type="xsd:string"/>
@@ -522,51 +527,54 @@ def test_invalid_input_parameter_type_handling():
             <xsd:element name="p2" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"), nosend=True, prettyxml=True)
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace)
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
 
     # Passing an unrelated Python type value.
     class SomeType:
         def __str__(self):
             return "Some string representation."
-    assert _compare_request(client.service.f(anInteger=SomeType()), """\
+    _assert_request_content(client.service.f(anInteger=SomeType()), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:p1/>
-         <ns0:anInteger>Some string representation.</ns0:anInteger>
-         <ns0:p2/>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <p1/>
+      <anInteger>Some string representation.</anInteger>
+      <p2/>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
     # Passing a value of a WSDL schema defined type.
-    value = client.factory.create("Freakazoid")
+    value = client.factory.create("my_xsd:Freakazoid")
     value.freak1 = "Tiny"
     value.freak2 = "Miny"
     value.freak3 = "Mo"
-    assert _compare_request(client.service.f(anInteger=value), """\
+    _assert_request_content(client.service.f(anInteger=value), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:p1/>
-         <ns0:anInteger>
-            <ns0:freak1>Tiny</ns0:freak1>
-            <ns0:freak2>Miny</ns0:freak2>
-            <ns0:freak3>Mo</ns0:freak3>
-         </ns0:anInteger>
-         <ns0:p2/>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <p1/>
+      <anInteger>
+        <freak1>Tiny</freak1>
+        <freak2>Miny</freak2>
+        <freak3>Mo</freak3>
+      </anInteger>
+      <p2/>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
 
 def test_missing_parameters():
     """Missing non-optional parameters should get passed as empty values."""
-    service = _service_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "plonker"
+    service = _service_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
@@ -574,67 +582,68 @@ def test_missing_parameters():
             <xsd:element name="anInteger" type="xsd:integer"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace))
 
-    assert _compare_request(service.f(), """\
+    _assert_request_content(service.f(), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString/>
-         <ns0:anInteger/>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-
-    assert _compare_request(service.f(u"Pero Ždero"), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString/>
+      <anInteger/>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+
+    _assert_request_content(service.f((u("Pero \u017Ddero"))), u("""\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString>Pero Ždero</ns0:aString>
-         <ns0:anInteger/>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-
-    assert _compare_request(service.f(anInteger=666), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString>Pero \u017Ddero</aString>
+      <anInteger/>
+    </Wrapper>
+  </Body>
+</Envelope>""") % (xsd_target_namespace,))
+
+    _assert_request_content(service.f(anInteger=666), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString/>
-         <ns0:anInteger>666</ns0:anInteger>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString/>
+      <anInteger>666</anInteger>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
     # None value is treated the same as undefined.
-    assert _compare_request(service.f(aString=None, anInteger=666), """\
+    _assert_request_content(service.f(aString=None, anInteger=666), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString/>
-         <ns0:anInteger>666</ns0:anInteger>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-    assert _compare_request(service.f(aString="Omega", anInteger=None), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString/>
+      <anInteger>666</anInteger>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+    _assert_request_content(service.f(aString="Omega", anInteger=None), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString>Omega</ns0:aString>
-         <ns0:anInteger/>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString>Omega</aString>
+      <anInteger/>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
 
 def test_named_parameter():
@@ -645,10 +654,11 @@ def test_named_parameter():
 
         def test(self, *args, **kwargs):
             request = self.service.f(*args, **kwargs)
-            assert _compare_request(request, self.expected_xml)
+            _assert_request_content(request, self.expected_xml)
 
     # Test different ways to make the same web service operation call.
-    service = _service_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "qwerty"
+    service = _service_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
@@ -656,26 +666,28 @@ def test_named_parameter():
             <xsd:element name="due" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace))
     t = Tester(service, """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:uno>einz</ns0:uno>
-         <ns0:due>zwei</ns0:due>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <uno>einz</uno>
+      <due>zwei</due>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
     t.test("einz", "zwei")
     t.test(uno="einz", due="zwei")
     t.test(due="zwei", uno="einz")
     t.test("einz", due="zwei")
 
-    #   The order of parameters in the constructed SOAP request should depend
+    # The order of parameters in the constructed SOAP request should depend
     # only on the initial WSDL schema.
-    service = _service_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "abracadabra"
+    service = _service_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
@@ -683,18 +695,19 @@ def test_named_parameter():
             <xsd:element name="uno" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace))
     t = Tester(service, """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:due>zwei</ns0:due>
-         <ns0:uno>einz</ns0:uno>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <due>zwei</due>
+      <uno>einz</uno>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
     t.test("zwei", "einz")
     t.test(uno="einz", due="zwei")
     t.test(due="zwei", uno="einz")
@@ -703,7 +716,8 @@ def test_named_parameter():
 
 def test_optional_parameter_handling():
     """Missing optional parameters should not get passed at all."""
-    service = _service_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "RoOfIe"
+    service = _service_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
@@ -711,81 +725,145 @@ def test_optional_parameter_handling():
             <xsd:element name="anInteger" type="xsd:integer" minOccurs="0"/>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper"))
+      </xsd:element>""", input="Wrapper", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace))
 
-    assert _compare_request(service.f(), """\
+    _assert_request_content(service.f(), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper/>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s"/>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
     # None is treated as an undefined value.
-    assert _compare_request(service.f(None), """\
+    _assert_request_content(service.f(None), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper/>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s"/>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
 
     # Empty string values are treated as well defined values.
-    assert _compare_request(service.f(""), """\
+    _assert_request_content(service.f(""), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString/>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+
+    _assert_request_content(service.f("Kiflica"), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString></ns0:aString>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-
-    assert _compare_request(service.f("Kiflica"), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString>Kiflica</aString>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+
+    _assert_request_content(service.f(anInteger=666), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString>Kiflica</ns0:aString>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-
-    assert _compare_request(service.f(anInteger=666), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <anInteger>666</anInteger>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+
+    _assert_request_content(service.f("Alfa", 9), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:anInteger>666</ns0:anInteger>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
-
-    assert _compare_request(service.f("Alfa", 9), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="%s">
+      <aString>Alfa</aString>
+      <anInteger>9</anInteger>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (xsd_target_namespace,))
+
+
+def test_SOAP_headers():
+    """Rudimentary 'soapheaders' option usage test."""
+    wsdl = suds.byte_str("""\
+<?xml version="1.0" encoding="utf-8"?>
+<wsdl:definitions targetNamespace="my-target-namespace"
+    xmlns:tns="my-target-namespace"
+    xmlns:s="http://www.w3.org/2001/XMLSchema"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
+
+  <wsdl:types>
+    <s:schema elementFormDefault="qualified"
+        targetNamespace="my-target-namespace">
+      <s:element name="MyHeader">
+        <s:complexType>
+          <s:sequence>
+            <s:element name="Freaky" type="s:hexBinary"/>
+          </s:sequence>
+        </s:complexType>
+      </s:element>
+    </s:schema>
+  </wsdl:types>
+
+  <wsdl:message name="myOperationHeader">
+    <wsdl:part name="MyHeader" element="tns:MyHeader"/>
+  </wsdl:message>
+
+  <wsdl:portType name="MyWSSOAP">
+    <wsdl:operation name="my_operation"/>
+  </wsdl:portType>
+
+  <wsdl:binding name="MyWSSOAP" type="tns:MyWSSOAP">
+    <soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="my_operation">
+      <soap:operation soapAction="my-SOAP-action" style="document"/>
+      <wsdl:input>
+        <soap:header message="tns:myOperationHeader" part="MyHeader"
+            use="literal"/>
+      </wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+
+  <wsdl:service name="MyWS">
+    <wsdl:port name="MyWSSOAP" binding="tns:MyWSSOAP">
+      <soap:address location="protocol://my-WS-URL"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""")
+    header_data = "fools rush in where angels fear to tread"
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    client.options.soapheaders = header_data
+    _assert_request_content(client.service.my_operation(), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:aString>Alfa</ns0:aString>
-         <ns0:anInteger>9</ns0:anInteger>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header>
+    <MyHeader xmlns="my-target-namespace">%s</MyHeader>
+  </Header>
+  <Body/>
+</Envelope>""" % (header_data,))
 
 
 def test_twice_wrapped_parameter():
     """
-      Suds does not recognize 'twice wrapped' data structures and unwraps the
+    Suds does not recognize 'twice wrapped' data structures and unwraps the
     external one but keeps the internal wrapping structure in place.
 
     """
-    client = tests.client_from_wsdl(tests.wsdl_input("""\
+    xsd_target_namespace = "spank me"
+    wsdl = testutils.wsdl("""\
       <xsd:element name="Wrapper1">
         <xsd:complexType>
           <xsd:sequence>
@@ -798,9 +876,11 @@ def test_twice_wrapped_parameter():
             </xsd:element>
           </xsd:sequence>
         </xsd:complexType>
-      </xsd:element>""", "Wrapper1"), nosend=True, prettyxml=True)
+      </xsd:element>""", input="Wrapper1", operation_name="f",
+        xsd_target_namespace=xsd_target_namespace)
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
 
-    assert _isInputWrapped(client, "f")
+    assert _is_input_wrapped(client, "f")
 
     # Web service operation calls made with 'valid' parameters.
     #
@@ -810,39 +890,41 @@ def test_twice_wrapped_parameter():
     # the current simpler solution is good enough for what we want to test
     # here.
     value = "A B C"
-    expectedRequest = """\
+    expected_request = """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper1>
-         <ns0:Wrapper2>%s</ns0:Wrapper2>
-      </ns0:Wrapper1>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""" % (value,)
-    assert _compare_request(client.service.f(value), expectedRequest)
-    assert _compare_request(client.service.f(Wrapper2=value), expectedRequest)
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper1 xmlns="%s">
+      <Wrapper2>%s</Wrapper2>
+    </Wrapper1>
+  </Body>
+</Envelope>""" % (xsd_target_namespace, value)
+    _assert_request_content(client.service.f(value), expected_request)
+    _assert_request_content(client.service.f(Wrapper2=value), expected_request)
 
     # Web service operation calls made with 'invalid' parameters.
-    def testInvalidParameter(**kwargs):
+    def test_invalid_parameter(**kwargs):
         assert len(kwargs) == 1
-        element = kwargs.keys()[0]
-        expected = "f() got an unexpected keyword argument '%s'" % (element,)
+        keyword = next(iterkeys(kwargs))
+        expected = "f() got an unexpected keyword argument '%s'" % (keyword,)
         e = pytest.raises(TypeError, client.service.f, **kwargs).value
         try:
             assert str(e) == expected
         finally:
-            del e
-    testInvalidParameter(Elemento="A B C")
-    testInvalidParameter(Wrapper1="A B C")
+            del e  # explicitly break circular reference chain in Python 3
+    test_invalid_parameter(Elemento=value)
+    test_invalid_parameter(Wrapper1=value)
 
 
 def test_wrapped_parameter(monkeypatch):
     monkeypatch.delitem(locals(), "e", False)
 
     # Prepare web service proxies.
-    client = lambda *args : tests.client_from_wsdl(tests.wsdl_input(*args),
-        nosend=True, prettyxml=True)
+    def client(xsd, *input):
+        wsdl = testutils.wsdl(xsd, input=input, xsd_target_namespace="toolyan",
+            operation_name="f")
+        return testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
     client_bare_single = client("""\
       <xsd:element name="Elemento" type="xsd:string"/>""", "Elemento")
     client_bare_multiple_simple = client("""\
@@ -855,8 +937,8 @@ def test_wrapped_parameter(monkeypatch):
           <xsd:element name="Elemento" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
-      <xsd:element name="Elemento1" type="ns:Wrapper"/>
-      <xsd:element name="Elemento2" type="ns:Wrapper"/>""", "Elemento1",
+      <xsd:element name="Elemento1" type="Wrapper"/>
+      <xsd:element name="Elemento2" type="Wrapper"/>""", "Elemento1",
         "Elemento2")
     client_wrapped_unnamed = client("""\
       <xsd:element name="Wrapper">
@@ -872,82 +954,89 @@ def test_wrapped_parameter(monkeypatch):
           <xsd:element name="Elemento" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
-      <xsd:element name="Wrapper" type="ns:WrapperType"/>""", "Wrapper")
+      <xsd:element name="Wrapper" type="WrapperType"/>""", "Wrapper")
 
-    #   Make sure suds library interprets our WSDL definitions as wrapped or
-    # bare input interfaces as expected.
-    assert not _isInputWrapped(client_bare_single, "f")
-    assert not _isInputWrapped(client_bare_multiple_simple, "f")
-    assert not _isInputWrapped(client_bare_multiple_wrapped, "f")
-    assert _isInputWrapped(client_wrapped_unnamed, "f")
-    assert _isInputWrapped(client_wrapped_named, "f")
+    # Make sure suds library interprets our WSDL definitions as wrapped or bare
+    # input interfaces as expected.
+    assert not _is_input_wrapped(client_bare_single, "f")
+    assert not _is_input_wrapped(client_bare_multiple_simple, "f")
+    assert not _is_input_wrapped(client_bare_multiple_wrapped, "f")
+    assert _is_input_wrapped(client_wrapped_unnamed, "f")
+    assert _is_input_wrapped(client_wrapped_named, "f")
 
-    #   Both bare & wrapped single parameter input web service operations get
+    # Both bare & wrapped single parameter input web service operations get
     # called the same way even though the wrapped one actually has an extra
     # wrapper element around its input data.
     data = "Maestro"
-    call_single = lambda c : c.service.f(data)
+    def call_single(c):
+        return c.service.f(data)
 
-    assert _compare_request(call_single(client_bare_single), """\
+    _assert_request_content(call_single(client_bare_single), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Elemento>%s</ns0:Elemento>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""" % data)
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Elemento xmlns="toolyan">%s</Elemento>
+  </Body>
+</Envelope>""" % (data,))
 
     expected_xml = """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Wrapper>
-         <ns0:Elemento>%s</ns0:Elemento>
-      </ns0:Wrapper>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""" % data
-    assert _compare_request(call_single(client_wrapped_unnamed), expected_xml)
-    assert _compare_request(call_single(client_wrapped_named), expected_xml)
-
-    #   Suds library's automatic structure unwrapping prevents us from
-    # specifying the external wrapper structure directly.
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body>
+    <Wrapper xmlns="toolyan">
+      <Elemento>%s</Elemento>
+    </Wrapper>
+  </Body>
+</Envelope>""" % (data,)
+    _assert_request_content(call_single(client_wrapped_unnamed), expected_xml)
+    _assert_request_content(call_single(client_wrapped_named), expected_xml)
+
+    # Suds library's automatic structure unwrapping prevents us from specifying
+    # the external wrapper structure directly.
     e = pytest.raises(TypeError, client_wrapped_unnamed.service.f, Wrapper="A")
-    assert str(e.value) == "f() got an unexpected keyword argument 'Wrapper'"
+    try:
+        expected = "f() got an unexpected keyword argument 'Wrapper'"
+        assert str(e.value) == expected
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
-    #   Multiple parameter web service operations are never automatically
+    # Multiple parameter web service operations are never automatically
     # unwrapped.
     data = ("Unga", "Bunga")
-    call_multiple = lambda c : c.service.f(*data)
+    def call_multiple(c):
+        return c.service.f(*data)
 
-    assert _compare_request(call_multiple(client_bare_multiple_simple), """\
+    _assert_request_content(call_multiple(client_bare_multiple_simple), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Elemento1>%s</ns0:Elemento1>
-      <ns0:Elemento2>%s</ns0:Elemento2>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""" % data)
-
-    assert _compare_request(call_multiple(client_bare_multiple_wrapped), """\
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body xmlns:ns="toolyan">
+    <ns:Elemento1>%s</ns:Elemento1>
+    <ns:Elemento2>%s</ns:Elemento2>
+  </Body>
+</Envelope>""" % data)
+
+    _assert_request_content(call_multiple(client_bare_multiple_wrapped), """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Elemento1>%s</ns0:Elemento1>
-      <ns0:Elemento2>%s</ns0:Elemento2>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""" % data)
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Header/>
+  <Body xmlns:ns="toolyan">
+    <ns:Elemento1>%s</ns:Elemento1>
+    <ns:Elemento2>%s</ns:Elemento2>
+  </Body>
+</Envelope>""" % data)
 
 
-def _compare_request(request, expected_xml):
-    return tests.compare_xml_to_string(request.original_envelope, expected_xml)
+def _assert_request_content(request, expected_xml):
+    CompareSAX.data2data(request.envelope, expected_xml)
 
 
-def _isInputWrapped(client, method_name):
+def _is_input_wrapped(client, method_name):
     assert len(client.wsdl.bindings) == 1
-    operation = client.wsdl.bindings.values()[0].operations[method_name]
+    binding = next(itervalues(client.wsdl.bindings))
+    operation = binding.operations[method_name]
     return operation.soap.input.body.wrapped
 
 
@@ -959,5 +1048,4 @@ def _service_from_wsdl(wsdl):
     invocation requests and does not attempt to actually send them.
 
     """
-    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
-    return client.service
+    return testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True).service
diff --git a/tests/test_sax_document.py b/tests/test_sax_document.py
new file mode 100644
index 0000000..c7c947a
--- /dev/null
+++ b/tests/test_sax_document.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds SAX Document unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import testutils
+    testutils.run_using_pytest(globals())
+
+import suds
+from suds.sax.document import Document
+import suds.sax.parser
+
+import pytest
+import six
+
+import re
+import sys
+
+
+class TestStringRepresentation:
+
+    @staticmethod
+    def create_test_document():
+        input_data = suds.byte_str("""\
+<xsd:element name="ZuZu">
+   <xsd:simpleType>
+      <xsd:restriction base="xsd:string">
+         <xsd:enumeration value="alfa"/>
+         <xsd:enumeration value="beta"/>
+         <xsd:enumeration value="gamma"/>
+      </xsd:restriction>
+   </xsd:simpleType>
+</xsd:element>""")
+        document = suds.sax.parser.Parser().parse(suds.BytesIO(input_data))
+        assert document.__class__ is Document
+        return document
+
+    @pytest.mark.skipif(sys.version_info >= (3,), reason="Python 2 specific")
+    def test_convert_to_byte_str(self):
+        document = self.create_test_document()
+        expected = suds.byte_str(document.str())
+        assert str(document) == expected
+
+    def test_convert_to_unicode(self):
+        document = self.create_test_document()
+        expected = document.str()
+        assert six.text_type(document) == expected
+
+    def test_plain_method(self):
+        document = self.create_test_document()
+        expected = Document.DECL + document.root().plain()
+        result = document.plain()
+        assert result == expected
+
+    def test_str_method(self):
+        document = self.create_test_document()
+        expected = Document.DECL + "\n" + document.root().str()
+        result = document.str()
+        assert result == expected
+
+    def test_xml_declaration(self):
+        assert Document.DECL == '<?xml version="1.0" encoding="UTF-8"?>'
diff --git a/tests/test_sax_element.py b/tests/test_sax_element.py
new file mode 100644
index 0000000..1bacce8
--- /dev/null
+++ b/tests/test_sax_element.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds SAX Element unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import testutils
+    testutils.run_using_pytest(globals())
+
+import suds
+from suds.sax.element import Element
+import suds.sax.parser
+
+import pytest
+import six
+
+import re
+import sys
+
+
+class TestChildAtPath:
+
+    def test_backslash_as_path_separator(self):
+        name1 = "child"
+        name2 = "grandchild"
+        root = self.__create_single_branch("root", name1, name2)[0]
+        result = root.childAtPath(name1 + "\\" + name2)
+        assert result is None
+
+    def test_backslash_in_name(self):
+        root, a, _, _ = self.__create_single_branch("root", "a", "b", "c")
+        b_c = Element("b\\c")
+        a.append(b_c)
+        result = root.childAtPath("a/b\\c")
+        assert result is b_c
+
+    def test_child_leaf(self):
+        root, child = self.__create_single_branch("root", "child")
+        result = root.childAtPath("child")
+        assert result is child
+
+    def test_child_not_leaf(self):
+        root, child, _ = self.__create_single_branch("root", "child",
+            "grandchild")
+        result = root.childAtPath("child")
+        assert result is child
+
+    def test_grandchild_leaf(self):
+        root, _, grandchild = self.__create_single_branch("root", "child",
+            "grandchild")
+        result = root.childAtPath("child/grandchild")
+        assert result is grandchild
+
+    def test_grandchild_not_leaf(self):
+        root, _, grandchild, _ = self.__create_single_branch("root", "child",
+            "grandchild", "great grandchild")
+        result = root.childAtPath("child/grandchild")
+        assert result is grandchild
+
+    def test_misplaced(self):
+        root = self.__create_single_branch("root", "a", "x", "b")[0]
+        result = root.childAtPath("a/b")
+        assert result is None
+
+    def test_missing(self):
+        root = Element("root")
+        result = root.childAtPath("an invalid path")
+        assert result is None
+
+    def test_name_including_spaces(self):
+        root, _, child, _ = self.__create_single_branch("root", "dumbo",
+            "foo  -  bar", "baz")
+        result = root.childAtPath("dumbo/foo  -  bar")
+        assert result is child
+
+    @pytest.mark.parametrize("n", (2, 3))
+    def test_repeated_path_separators(self, n):
+        root, child, grandchild = self.__create_single_branch("root", "child",
+            "grandchild")
+        sep = "/" * n
+        path = "child" + sep + "grandchild"
+        result = root.childAtPath(path)
+        assert result is grandchild
+
+    def test_same_named(self):
+        root, _, child, _ = self.__create_single_branch("root", "a", "a", "a")
+        result = root.childAtPath("a/a")
+        assert result is child
+
+    @staticmethod
+    def __create_single_branch(*args):
+        """
+        Construct a single branch element tree with given element names.
+
+        Returns a list of constructed Element nodes from root to leaf.
+
+        """
+        result = []
+        parent = None
+        for name in args:
+            e = Element(name)
+            result.append(e)
+            if parent is not None:
+                parent.append(e)
+            parent = e
+        return result
+
+
+class TestStringRepresentation:
+
+    # Must be consistent with how Element.str() formats this data.
+    str_formatted_xml = """\
+<xsd:element name="ZuZu">
+   <xsd:simpleType>
+      <xsd:restriction base="xsd:string">
+         <xsd:enumeration value="alfa"/>
+         <xsd:enumeration value="beta"/>
+         <xsd:enumeration value="gamma"/>
+      </xsd:restriction>
+   </xsd:simpleType>
+</xsd:element>"""
+
+    @staticmethod
+    def create_test_element(content=str_formatted_xml):
+        input_data = suds.byte_str(content)
+        xml = suds.sax.parser.Parser().parse(suds.BytesIO(input_data))
+        element = xml.root()
+        assert element.__class__ is Element
+        return element
+
+    @pytest.mark.skipif(sys.version_info >= (3,), reason="Python 2 specific")
+    def test_convert_to_byte_str(self):
+        element = self.create_test_element()
+        expected = suds.byte_str(element.str())
+        assert str(element) == expected
+
+    def test_convert_to_unicode(self):
+        element = self.create_test_element()
+        expected = element.str()
+        assert six.text_type(element) == expected
+
+    def test_plain_method(self):
+        element = self.create_test_element(self.str_formatted_xml)
+        expected = re.sub("\s*[\r\n]\s*", "", self.str_formatted_xml)
+        result = element.plain()
+        assert result == expected
+
+    def test_str_method(self):
+        element = self.create_test_element(self.str_formatted_xml)
+        result = element.str()
+        assert result == self.str_formatted_xml
+
+
+ at pytest.mark.parametrize("name, expected_prefix, expected_name", (
+    ("", None, ""),
+    ("bazinga", None, "bazinga"),
+    ("test element name", None, "test element name"),
+    ("aaa:bbb", "aaa", "bbb"),
+    ("aaa:", "aaa", ""),
+    (":aaa", "", "aaa"),
+    ("aaa::bbb", "aaa", ":bbb"),
+    ("aaa:bbb:ccc", "aaa", "bbb:ccc")))
+def test_init_name(name, expected_prefix, expected_name):
+    e = Element(name)
+    assert e.prefix == expected_prefix
+    assert e.name == expected_name
diff --git a/tests/test_sax_encoder.py b/tests/test_sax_encoder.py
new file mode 100644
index 0000000..f7d1f37
--- /dev/null
+++ b/tests/test_sax_encoder.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds SAX module's special character encoder unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import testutils
+    testutils.run_using_pytest(globals())
+
+import suds.sax.enc
+
+import pytest
+
+
+"""Data not affected by encode()/decode() operations."""
+invariant_decoded_encoded_test_data = (
+    # Empty text.
+    "",
+    # Pure text.
+    "x",
+    "xyz",
+    "Devil take the hindmost",
+    # Whitespace handling.
+    "spaces    kept",
+    " ",
+    "  ",
+    "\t",
+    "\v",
+    "\f",
+    "\r",
+    "\n",
+    "  \t\t\v\v   \f\f  \v\v\f\r\n\n\r  \t \t\t",
+    "  \t\f\v   something   \r\r\n\freal\f\f\r\n\f\t\f")
+
+"""
+Data that should not be affected by encode()/decode() operations but for which
+the current encode()/decode() operation implementations are broken.
+
+"""
+invariant_decoded_encoded_test_data__broken = (
+    # CDATA handling.
+    "<![CDATA['\"&<> \t\r\n\v\f < > ' " &]]>",
+    "<![CDATA[\\'\\\"\\&\\<\\> \\< \\> \\' \\" ]\&]]>",
+    "<![CDATA[&&<><><><>>>>><<<< This is !!!&& & a test...."
+        "<<>>>>>>>>}}>]?]>>>>]]>")
+
+"""
+Data that should not be affected by encode()/decode() operations but for which
+the current encode() operation implementation is broken.
+
+"""
+invariant_decoded_encoded_test_data__broken_encode = (
+    # CDATA handling.
+    "<![CDATA[]]>",
+    "<![CDATA[wonderful]]>",
+    "<![CDATA[&]] >]]>",
+    "<![CDATA[\"It's a wonderful life!\", said the smurf.]]>",
+    "<![CDATA[<![CDATA[<![CDATA[<![CDATA[]]>",
+    "<![CDATA[<![CDATA[]]]]><![CDATA[>]]>")
+
+"""
+Decoded/encoded data convertible in either direction using encode()/decode()
+operations.
+
+"""
+symmetric_decoded_encoded_test_data = [
+    # Simple replacements.
+    ("<", "<"),
+    (">", ">"),
+    ("'", "'"),
+    ('"', """),
+    ("&", "&"),
+    # Mixed replacements.
+    ("abcd&&<<", "abcd&&<<"),
+    # Character reference lookalikes.
+    ("& lt;", "& lt;"),
+    ("&gt ;", "&gt ;"),
+    ("&a pos;", "&a pos;"),
+    ("&walle;", "&walle;"),
+    ("&quot", "&quot"),
+    ("&quo", "&quo"),
+    ("amp;", "amp;"),
+    # XML markup.
+    ("<a>unga-bunga</a>", "<a>unga-bunga</a>"),
+    ("<a></b>", "<a></b>"),
+    ("<&></\n>", "<&></\n>"),
+    ("<a id=\"fluffy's\"> && </a>",
+        "<a id="fluffy's"> && </a>"),
+    # CDATA handling.
+    ("< ![CDATA[&]]>", "< ![CDATA[&]]>"),
+    ("<! [CDATA[&]]>", "<! [CDATA[&]]>"),
+    ("<![ CDATA[&]]>", "<![ CDATA[&]]>"),
+    ("<![CDATA [&]]>", "<![CDATA [&]]>")] + [
+    # Invarant data.
+    (x, x) for x in invariant_decoded_encoded_test_data]
+
+"""
+Decoded/encoded data that should be convertible in either direction using
+encode()/decode() operations but for which the current encode()/decode()
+operation implementations are broken.
+
+"""
+symmetric_decoded_encoded_test_data__broken = [
+    # CDATA handling.
+    (x, x) for x in invariant_decoded_encoded_test_data__broken]
+
+"""
+Decoded/encoded data that should be convertible in either direction using
+encode()/decode() operations but for which the current encode() operation
+implementation is broken.
+
+"""
+symmetric_decoded_encoded_test_data__broken_encode = [
+    # CDATA handling.
+    (x, x) for x in invariant_decoded_encoded_test_data__broken_encode] + [
+    ("<a><![CDATA[<a></a>]]></a>", "<a><![CDATA[<a></a>]]></a>"),
+    ("&<![CDATA[&<![CDATA[&]]>&]]>&",
+        "&<![CDATA[&<![CDATA[&]]>&]]>&")]
+
+
+ at pytest.mark.parametrize(("input", "expected"), [
+    (e, d) for d, e in
+        symmetric_decoded_encoded_test_data +
+        symmetric_decoded_encoded_test_data__broken_encode] + [
+    pytest.mark.xfail((e, d), reason="CDATA encoding not supported yet")
+        for d, e in symmetric_decoded_encoded_test_data__broken] + [
+    # Character reference lookalikes.
+    (x, x) for x in (
+        "& lt;",
+        "&gt ;",
+        "&a pos;",
+        "&walle;",
+        "&quot",
+        "&quo",
+        "amp;")] + [
+    # Double decode.
+    ("&amp;", "&"),
+    ("&lt;", "<"),
+    ("&gt;", ">"),
+    ("&apos;", "'"),
+    ("&quot;", """)])
+def test_decode(input, expected):
+    assert suds.sax.enc.Encoder().decode(input) == expected
+
+
+ at pytest.mark.parametrize(("input", "expected"),
+    symmetric_decoded_encoded_test_data + [
+    pytest.mark.xfail(x, reason="CDATA encoding not supported yet") for x in
+        symmetric_decoded_encoded_test_data__broken +
+        symmetric_decoded_encoded_test_data__broken_encode] + [
+    # Double encoding.
+    #TODO: See whether this 'avoid double encoding' behaviour is actually
+    # desirable. That is how XML entity reference encoding has been implemented
+    # in the original suds implementation, but it makes encode/decode
+    # operations asymmetric and prevents the user from actually encoding data
+    # like '&' that should not be interpreted as '&'.
+    ("&", "&"),
+    ("<", "<"),
+    (">", ">"),
+    ("'", "'"),
+    (""", """)])
+def test_encode(input, expected):
+    assert suds.sax.enc.Encoder().encode(input) == expected
diff --git a/tests/test_suds.py b/tests/test_suds.py
index 614c976..3c6785e 100644
--- a/tests/test_suds.py
+++ b/tests/test_suds.py
@@ -1,19 +1,18 @@
 # -*- coding: utf-8 -*-
 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
@@ -26,22 +25,22 @@ tests get added to it and it acquires more structure.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
-import tests
+from testutils.compare_sax import CompareSAX
 
 import pytest
+from six import itervalues, next
 
 import re
 import xml.sax
 
 
-# TODO: Update the current choice parameter handling implementation to make
-# this test pass.
+#TODO: Update the current choice parameter handling implementation to make this
+# test pass.
 @pytest.mark.xfail
 def test_choice_parameter_implementation_inconsistencies():
     """
@@ -58,15 +57,16 @@ def test_choice_parameter_implementation_inconsistencies():
     constructed parameter definition structure.
 
     """
-    client = lambda x, y : tests.client_from_wsdl(tests.wsdl_input(x, y))
+    def client(x, y):
+        return testutils.client_from_wsdl(testutils.wsdl(x, input=y))
 
     client_simple_short = client("""\
-      <xsd:element name="Elemento" type="xsd:string" />""", "Elemento")
+      <xsd:element name="Elemento" type="xsd:string"/>""", "Elemento")
 
     client_simple_long = client("""\
       <xsd:element name="Elemento">
         <xsd:simpleType>
-          <xsd:restriction base="xsd:string" />
+          <xsd:restriction base="xsd:string"/>
         </xsd:simpleType>
       </xsd:element>""", "Elemento")
 
@@ -74,7 +74,7 @@ def test_choice_parameter_implementation_inconsistencies():
       <xsd:element name="Wrapper">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="Elemento" type="xsd:string" />
+            <xsd:element name="Elemento" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>""", "Wrapper")
@@ -89,13 +89,13 @@ def test_choice_parameter_implementation_inconsistencies():
 
 
 def test_converting_client_to_string_must_not_raise_an_exception():
-    client = tests.client_from_wsdl(suds.byte_str(
-        "<?xml version='1.0' encoding='UTF-8'?><root />"))
+    client = testutils.client_from_wsdl(suds.byte_str(
+        "<?xml version='1.0' encoding='UTF-8'?><root/>"))
     str(client)
 
 
 def test_converting_metadata_to_string():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -108,9 +108,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="AAA">
         <xsd:sequence>
-          <xsd:element name="u1" type="xsd:string" />
-          <xsd:element name="u2" type="xsd:string" />
-          <xsd:element name="u3" type="xsd:string" />
+          <xsd:element name="u1" type="xsd:string"/>
+          <xsd:element name="u2" type="xsd:string"/>
+          <xsd:element name="u3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
     </xsd:schema>
@@ -119,11 +119,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -141,22 +141,26 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert re.search(" ordering\[\] = ", metadata_string)
 
 
-def test_empty_invalid_wsdl(monkeypatch):
+def test_empty_invalid_WSDL(monkeypatch):
     wsdl = suds.byte_str("")
     monkeypatch.delitem(locals(), "e", False)
-    e = pytest.raises(xml.sax.SAXParseException, tests.client_from_wsdl, wsdl)
-    assert e.value.getMessage() == "no element found"
+    e = pytest.raises(xml.sax.SAXParseException, testutils.client_from_wsdl,
+        wsdl)
+    try:
+        assert e.value.getMessage() == "no element found"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
 
-def test_empty_valid_wsdl():
-    client = tests.client_from_wsdl(suds.byte_str(
-        "<?xml version='1.0' encoding='UTF-8'?><root />"))
+def test_empty_valid_WSDL():
+    client = testutils.client_from_wsdl(suds.byte_str(
+        "<?xml version='1.0' encoding='UTF-8'?><root/>"))
     assert not client.wsdl.services, "No service definitions must be read "  \
         "from an empty WSDL."
 
 
 def test_enumeration_type_string_should_contain_its_value():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -169,9 +173,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:simpleType name="AAA">
         <xsd:restriction base="xsd:string">
-          <xsd:enumeration value="One" />
-          <xsd:enumeration value="Two" />
-          <xsd:enumeration value="Thirty-Two" />
+          <xsd:enumeration value="One"/>
+          <xsd:enumeration value="Two"/>
+          <xsd:enumeration value="Thirty-Two"/>
         </xsd:restriction>
       </xsd:simpleType>
     </xsd:schema>
@@ -180,11 +184,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -200,9 +204,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert e1.name == "One"
     assert e2.name == "Two"
     assert e3.name == "Thirty-Two"
-    #   Python 3 output does not include a trailing L after long integer
-    # output, while Python 2 does. For example: 0x12345678 is output as
-    # 0x12345678L in Python 2 and simply as 0x12345678 in Python 3.
+    # Python 3 output does not include a trailing L after long integer output,
+    # while Python 2 does. For example: 0x12345678 is output as 0x12345678L in
+    # Python 2 and simply as 0x12345678 in Python 3.
     assert re.match('<Enumeration:0x[0-9a-f]+L? name="One" />$', e1.str())
     assert re.match('<Enumeration:0x[0-9a-f]+L? name="Two" />$', e2.str())
     assert re.match('<Enumeration:0x[0-9a-f]+L? name="Thirty-Two" />$',
@@ -210,7 +214,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_function_parameters_global_sequence_in_a_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -223,42 +227,42 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="UngaBunga">
         <xsd:sequence>
-          <xsd:element name="u1" type="xsd:string" />
-          <xsd:element name="u2" type="xsd:string" />
-          <xsd:element name="u3" type="xsd:string" />
+          <xsd:element name="u1" type="xsd:string"/>
+          <xsd:element name="u2" type="xsd:string"/>
+          <xsd:element name="u3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
-            <xsd:element name="x2" type="UngaBunga" />
-            <xsd:element name="x3" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
+            <xsd:element name="x2" type="UngaBunga"/>
+            <xsd:element name="x3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Elemento" />
+    <wsdl:part name="parameters" element="ns:Elemento"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -295,7 +299,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_function_parameters_local_choice():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -309,33 +313,33 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:choice>
-            <xsd:element name="u1" type="xsd:string" />
-            <xsd:element name="u2" type="xsd:string" />
+            <xsd:element name="u1" type="xsd:string"/>
+            <xsd:element name="u2" type="xsd:string"/>
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Elemento" />
+    <wsdl:part name="parameters" element="ns:Elemento"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -366,13 +370,13 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert method_params[1][1] is service.params[1][0]
 
     # Construct method parameter element object.
-    paramOut = client.factory.create("Elemento")
-    _assert_dynamic_type(paramOut, "Elemento")
-    assert not paramOut.__keylist__
+    param_out = client.factory.create("Elemento")
+    _assert_dynamic_type(param_out, "Elemento")
+    assert not param_out.__keylist__
 
 
 def test_function_parameters_local_choice_in_a_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -386,42 +390,42 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
             <xsd:element name="x2">
               <xsd:complexType>
                 <xsd:choice>
-                  <xsd:element name="u1" type="xsd:string" />
-                  <xsd:element name="u2" type="xsd:string" />
-                  <xsd:element name="u3" type="xsd:string" />
+                  <xsd:element name="u1" type="xsd:string"/>
+                  <xsd:element name="u2" type="xsd:string"/>
+                  <xsd:element name="u3" type="xsd:string"/>
                 </xsd:choice>
               </xsd:complexType>
             </xsd:element>
-            <xsd:element name="x3" type="xsd:string" />
+            <xsd:element name="x3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Elemento" />
+    <wsdl:part name="parameters" element="ns:Elemento"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -457,22 +461,22 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert method_params[2][1] is service.params[2][0]
 
     # Construct method parameter element object.
-    paramOut = client.factory.create("Elemento")
-    _assert_dynamic_type(paramOut, "Elemento")
-    assert paramOut.x1 is None
-    _assert_dynamic_type(paramOut.x2, "x2")
-    assert not paramOut.x2.__keylist__
-    assert paramOut.x3 is None
+    param_out = client.factory.create("Elemento")
+    _assert_dynamic_type(param_out, "Elemento")
+    assert param_out.x1 is None
+    _assert_dynamic_type(param_out.x2, "x2")
+    assert not param_out.x2.__keylist__
+    assert param_out.x3 is None
 
     # Construct method parameter objects with a locally defined type.
-    paramIn = client.factory.create("Elemento.x2")
-    _assert_dynamic_type(paramIn, "x2")
-    assert not paramOut.x2.__keylist__
-    assert paramIn is not paramOut.x2
+    param_in = client.factory.create("Elemento.x2")
+    _assert_dynamic_type(param_in, "x2")
+    assert not param_out.x2.__keylist__
+    assert param_in is not param_out.x2
 
 
 def test_function_parameters_local_sequence_in_a_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -486,42 +490,42 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
             <xsd:element name="x2">
               <xsd:complexType>
                 <xsd:sequence>
-                  <xsd:element name="u1" type="xsd:string" />
-                  <xsd:element name="u2" type="xsd:string" />
-                  <xsd:element name="u3" type="xsd:string" />
+                  <xsd:element name="u1" type="xsd:string"/>
+                  <xsd:element name="u2" type="xsd:string"/>
+                  <xsd:element name="u3" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
             </xsd:element>
-            <xsd:element name="x3" type="xsd:string" />
+            <xsd:element name="x3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Elemento" />
+    <wsdl:part name="parameters" element="ns:Elemento"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -557,26 +561,26 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert method_params[2][1] is service.params[2][0]
 
     # Construct method parameter element object.
-    paramOut = client.factory.create("Elemento")
-    _assert_dynamic_type(paramOut, "Elemento")
-    assert paramOut.x1 is None
-    _assert_dynamic_type(paramOut.x2, "x2")
-    assert paramOut.x2.u1 is None
-    assert paramOut.x2.u2 is None
-    assert paramOut.x2.u3 is None
-    assert paramOut.x3 is None
+    param_out = client.factory.create("Elemento")
+    _assert_dynamic_type(param_out, "Elemento")
+    assert param_out.x1 is None
+    _assert_dynamic_type(param_out.x2, "x2")
+    assert param_out.x2.u1 is None
+    assert param_out.x2.u2 is None
+    assert param_out.x2.u3 is None
+    assert param_out.x3 is None
 
     # Construct method parameter objects with a locally defined type.
-    paramIn = client.factory.create("Elemento.x2")
-    _assert_dynamic_type(paramIn, "x2")
-    assert paramIn.u1 is None
-    assert paramIn.u2 is None
-    assert paramIn.u3 is None
-    assert paramIn is not paramOut.x2
+    param_in = client.factory.create("Elemento.x2")
+    _assert_dynamic_type(param_in, "x2")
+    assert param_in.u1 is None
+    assert param_in.u2 is None
+    assert param_in.u3 is None
+    assert param_in is not param_out.x2
 
 
 def test_function_parameters_sequence_in_a_choice():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -590,42 +594,42 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="Choice">
         <xsd:complexType>
           <xsd:choice>
-            <xsd:element name="a1" type="xsd:string" />
+            <xsd:element name="a1" type="xsd:string"/>
             <xsd:element name="sequence">
               <xsd:complexType>
                 <xsd:sequence>
-                  <xsd:element name="e1" type="xsd:string" />
-                  <xsd:element name="e2" type="xsd:string" />
-                  <xsd:element name="e3" type="xsd:string" />
+                  <xsd:element name="e1" type="xsd:string"/>
+                  <xsd:element name="e2" type="xsd:string"/>
+                  <xsd:element name="e3" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
             </xsd:element>
-            <xsd:element name="a2" type="xsd:string" />
+            <xsd:element name="a2" type="xsd:string"/>
           </xsd:choice>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Choice" />
+    <wsdl:part name="parameters" element="ns:Choice"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -633,39 +637,39 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
     # Input #1.
     request = _construct_SOAP_request(client, 'f', a1="Wackadoodle")
-    assert tests.compare_xml_to_string(request, """\
+    CompareSAX.document2data(request, """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Choice>
-         <ns0:a1>Wackadoodle</ns0:a1>
-      </ns0:Choice>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+   <Header/>
+   <Body>
+      <Choice xmlns="my-namespace">
+         <a1>Wackadoodle</a1>
+      </Choice>
+   </Body>
+</Envelope>""")
 
     # Input #2.
     param = client.factory.create("Choice.sequence")
     param.e2 = "Wackadoodle"
     request = _construct_SOAP_request(client, 'f', sequence=param)
-    assert tests.compare_xml_to_string(request, """\
+    CompareSAX.document2data(request, """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:Choice>
-         <ns0:sequence>
-            <ns0:e1/>
-            <ns0:e2>Wackadoodle</ns0:e2>
-            <ns0:e3/>
-         </ns0:sequence>
-      </ns0:Choice>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+   <Header/>
+   <Body>
+      <Choice xmlns="my-namespace">
+         <sequence>
+            <e1/>
+            <e2>Wackadoodle</e2>
+            <e3/>
+         </sequence>
+      </Choice>
+   </Body>
+</Envelope>""")
 
 
 def test_function_parameters_sequence_in_a_choice_in_a_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -682,17 +686,17 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
             <xsd:element name="choice">
               <xsd:complexType>
                 <xsd:choice>
-                  <xsd:element name="a1" type="xsd:string" />
+                  <xsd:element name="a1" type="xsd:string"/>
                   <xsd:element name="sequence">
                     <xsd:complexType>
                       <xsd:sequence>
-                        <xsd:element name="e1" type="xsd:string" />
-                        <xsd:element name="e2" type="xsd:string" />
-                        <xsd:element name="e3" type="xsd:string" />
+                        <xsd:element name="e1" type="xsd:string"/>
+                        <xsd:element name="e2" type="xsd:string"/>
+                        <xsd:element name="e3" type="xsd:string"/>
                       </xsd:sequence>
                     </xsd:complexType>
                   </xsd:element>
-                  <xsd:element name="a2" type="xsd:string" />
+                  <xsd:element name="a2" type="xsd:string"/>
                 </xsd:choice>
               </xsd:complexType>
             </xsd:element>
@@ -702,25 +706,25 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:External" />
+    <wsdl:part name="parameters" element="ns:External"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -733,26 +737,26 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
     # Construct a SOAP request containing our input parameters.
     request = _construct_SOAP_request(client, 'f', param)
-    assert tests.compare_xml_to_string(request, """\
+    CompareSAX.document2data(request, """\
 <?xml version="1.0" encoding="UTF-8"?>
-<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
-   <SOAP-ENV:Header/>
-   <ns1:Body>
-      <ns0:External>
-         <ns0:choice>
-            <ns0:sequence>
-               <ns0:e1/>
-               <ns0:e2>Wackadoodle</ns0:e2>
-               <ns0:e3/>
-            </ns0:sequence>
-         </ns0:choice>
-      </ns0:External>
-   </ns1:Body>
-</SOAP-ENV:Envelope>""")
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+   <Header/>
+   <Body>
+      <External xmlns="my-namespace">
+         <choice>
+            <sequence>
+               <e1/>
+               <e2>Wackadoodle</e2>
+               <e3/>
+            </sequence>
+         </choice>
+      </External>
+   </Body>
+</Envelope>""")
 
 
 def test_function_parameters_strings():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -766,34 +770,34 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
-            <xsd:element name="x2" type="xsd:string" />
-            <xsd:element name="x3" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
+            <xsd:element name="x2" type="xsd:string"/>
+            <xsd:element name="x3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:Elemento" />
+    <wsdl:part name="parameters" element="ns:Elemento"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -830,7 +834,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_global_enumeration():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -843,9 +847,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:simpleType name="AAA">
         <xsd:restriction base="xsd:string">
-          <xsd:enumeration value="One" />
-          <xsd:enumeration value="Two" />
-          <xsd:enumeration value="Thirty-Two" />
+          <xsd:enumeration value="One"/>
+          <xsd:enumeration value="Two"/>
+          <xsd:enumeration value="Thirty-Two"/>
         </xsd:restriction>
       </xsd:simpleType>
     </xsd:schema>
@@ -854,11 +858,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -893,7 +897,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_global_sequence_in_a_global_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -906,16 +910,16 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="Oklahoma">
         <xsd:sequence>
-          <xsd:element name="c1" type="xsd:string" />
-          <xsd:element name="c2" type="xsd:string" />
-          <xsd:element name="c3" type="xsd:string" />
+          <xsd:element name="c1" type="xsd:string"/>
+          <xsd:element name="c2" type="xsd:string"/>
+          <xsd:element name="c3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
       <xsd:complexType name="Wackadoodle">
         <xsd:sequence>
-          <xsd:element name="x1" type="xsd:string" />
-          <xsd:element name="x2" type="Oklahoma" />
-          <xsd:element name="x3" type="xsd:string" />
+          <xsd:element name="x1" type="xsd:string"/>
+          <xsd:element name="x2" type="Oklahoma"/>
+          <xsd:element name="x3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
     </xsd:schema>
@@ -924,11 +928,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -942,20 +946,20 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
         assert len(typeTuple) == 2
         assert typeTuple[0] is typeTuple[1]
 
-    aTypeIn = service.types[0][0]
-    assert isinstance(aTypeIn, suds.xsd.sxbasic.Complex)
-    assert aTypeIn.name == "Oklahoma"
-    assert not aTypeIn.sequence()
-    assert aTypeIn.rawchildren[0].sequence()
+    type_in = service.types[0][0]
+    assert isinstance(type_in, suds.xsd.sxbasic.Complex)
+    assert type_in.name == "Oklahoma"
+    assert not type_in.sequence()
+    assert type_in.rawchildren[0].sequence()
 
-    aTypeOut = service.types[1][0]
-    assert isinstance(aTypeOut, suds.xsd.sxbasic.Complex)
-    assert aTypeOut.name == "Wackadoodle"
-    assert not aTypeOut.sequence()
-    assert aTypeOut.rawchildren[0].sequence()
+    type_out = service.types[1][0]
+    assert isinstance(type_out, suds.xsd.sxbasic.Complex)
+    assert type_out.name == "Wackadoodle"
+    assert not type_out.sequence()
+    assert type_out.rawchildren[0].sequence()
 
-    assert len(aTypeOut.rawchildren) == 1
-    children = aTypeOut.children()
+    assert len(type_out.rawchildren) == 1
+    children = type_out.children()
     assert isinstance(children, list)
     assert len(children) == 3
     assert children[0][0].name == "x1"
@@ -965,21 +969,21 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert children[2][0].name == "x3"
     assert children[2][0].type == _string_type
 
-    sequenceOut = client.factory.create("Wackadoodle")
-    _assert_dynamic_type(sequenceOut, "Wackadoodle")
-    assert sequenceOut.__metadata__.sxtype is aTypeOut
-    assert sequenceOut.x1 is None
-    sequenceIn = sequenceOut.x2
-    assert sequenceOut.x3 is None
-    _assert_dynamic_type(sequenceIn, "Oklahoma")
-    assert sequenceIn.__metadata__.sxtype is aTypeIn
-    assert sequenceIn.c1 is None
-    assert sequenceIn.c2 is None
-    assert sequenceIn.c3 is None
+    sequence_out = client.factory.create("Wackadoodle")
+    _assert_dynamic_type(sequence_out, "Wackadoodle")
+    assert sequence_out.__metadata__.sxtype is type_out
+    assert sequence_out.x1 is None
+    sequence_in = sequence_out.x2
+    assert sequence_out.x3 is None
+    _assert_dynamic_type(sequence_in, "Oklahoma")
+    assert sequence_in.__metadata__.sxtype is type_in
+    assert sequence_in.c1 is None
+    assert sequence_in.c2 is None
+    assert sequence_in.c3 is None
 
 
 def test_global_string_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -992,9 +996,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="Oklahoma">
         <xsd:sequence>
-          <xsd:element name="c1" type="xsd:string" />
-          <xsd:element name="c2" type="xsd:string" />
-          <xsd:element name="c3" type="xsd:string" />
+          <xsd:element name="c1" type="xsd:string"/>
+          <xsd:element name="c2" type="xsd:string"/>
+          <xsd:element name="c3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
     </xsd:schema>
@@ -1003,11 +1007,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1061,7 +1065,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_local_sequence_in_a_global_sequence():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1077,16 +1081,16 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
           <xsd:element name="x1">
               <xsd:complexType name="Oklahoma">
                 <xsd:sequence>
-                  <xsd:element name="c1" type="xsd:string" />
-                  <xsd:element name="c2" type="xsd:string" />
-                  <xsd:element name="c3" type="xsd:string" />
+                  <xsd:element name="c1" type="xsd:string"/>
+                  <xsd:element name="c2" type="xsd:string"/>
+                  <xsd:element name="c3" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
           </xsd:element>
           <xsd:element name="x2">
               <xsd:complexType>
                 <xsd:sequence>
-                  <xsd:element name="s" type="xsd:string" />
+                  <xsd:element name="s" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
           </xsd:element>
@@ -1098,11 +1102,11 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1111,48 +1115,48 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     service = client.sd[0]
     assert len(service.types) == 1
 
-    aTypeOut = service.types[0][0]
-    assert isinstance(aTypeOut, suds.xsd.sxbasic.Complex)
-    assert aTypeOut.name == "Wackadoodle"
-    assert not aTypeOut.sequence()
-    assert aTypeOut.rawchildren[0].sequence()
+    type_out = service.types[0][0]
+    assert isinstance(type_out, suds.xsd.sxbasic.Complex)
+    assert type_out.name == "Wackadoodle"
+    assert not type_out.sequence()
+    assert type_out.rawchildren[0].sequence()
 
-    children = aTypeOut.children()
+    children = type_out.children()
     assert isinstance(children, list)
     assert len(children) == 2
-    aTypeIn1 = children[0][0]
-    assert isinstance(aTypeIn1, suds.xsd.sxbasic.Element)
-    assert not aTypeIn1.sequence()
-    assert aTypeIn1.rawchildren[0].rawchildren[0].sequence()
-    aTypeIn2 = children[1][0]
-    assert isinstance(aTypeIn2, suds.xsd.sxbasic.Element)
-    assert not aTypeIn2.sequence()
-    assert aTypeIn2.rawchildren[0].rawchildren[0].sequence()
-    assert aTypeIn1.rawchildren[0].name == "Oklahoma"
-    assert aTypeIn1.rawchildren[0].type is None
-    namespace1 = aTypeIn1.rawchildren[0].namespace()
+    type_in1 = children[0][0]
+    assert isinstance(type_in1, suds.xsd.sxbasic.Element)
+    assert not type_in1.sequence()
+    assert type_in1.rawchildren[0].rawchildren[0].sequence()
+    type_in2 = children[1][0]
+    assert isinstance(type_in2, suds.xsd.sxbasic.Element)
+    assert not type_in2.sequence()
+    assert type_in2.rawchildren[0].rawchildren[0].sequence()
+    assert type_in1.rawchildren[0].name == "Oklahoma"
+    assert type_in1.rawchildren[0].type is None
+    namespace1 = type_in1.rawchildren[0].namespace()
     assert namespace1 == ("ns", "my-namespace")
-    assert aTypeIn2.rawchildren[0].name is None
-    assert aTypeIn2.rawchildren[0].type is None
-    assert aTypeIn1.rawchildren[0].namespace() is namespace1
-
-    sequenceOut = client.factory.create("Wackadoodle")
-    _assert_dynamic_type(sequenceOut, "Wackadoodle")
-    assert sequenceOut.__metadata__.sxtype is aTypeOut
-    sequenceIn1 = sequenceOut.x1
-    sequenceIn2 = sequenceOut.x2
-    _assert_dynamic_type(sequenceIn1, "x1")
-    _assert_dynamic_type(sequenceIn2, "x2")
-    assert sequenceIn1.__metadata__.sxtype is aTypeIn1
-    assert sequenceIn2.__metadata__.sxtype is aTypeIn2
-    assert sequenceIn1.c1 is None
-    assert sequenceIn1.c2 is None
-    assert sequenceIn1.c3 is None
-    assert sequenceIn2.s is None
+    assert type_in2.rawchildren[0].name is None
+    assert type_in2.rawchildren[0].type is None
+    assert type_in1.rawchildren[0].namespace() is namespace1
+
+    sequence_out = client.factory.create("Wackadoodle")
+    _assert_dynamic_type(sequence_out, "Wackadoodle")
+    assert sequence_out.__metadata__.sxtype is type_out
+    sequence_in1 = sequence_out.x1
+    sequence_in2 = sequence_out.x2
+    _assert_dynamic_type(sequence_in1, "x1")
+    _assert_dynamic_type(sequence_in2, "x2")
+    assert sequence_in1.__metadata__.sxtype is type_in1
+    assert sequence_in2.__metadata__.sxtype is type_in2
+    assert sequence_in1.c1 is None
+    assert sequence_in1.c2 is None
+    assert sequence_in1.c3 is None
+    assert sequence_in2.s is None
 
 
 def test_no_trailing_comma_in_function_prototype_description_string__0():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1165,31 +1169,31 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:element name="InputData">
         <xsd:complexType>
-          <xsd:sequence />
+          <xsd:sequence/>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:InputData" />
+    <wsdl:part name="parameters" element="ns:InputData"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1199,7 +1203,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_no_trailing_comma_in_function_prototype_description_string__1():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1213,32 +1217,32 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="InputData">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:InputData" />
+    <wsdl:part name="parameters" element="ns:InputData"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1248,7 +1252,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_no_trailing_comma_in_function_prototype_description_string__3():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1262,34 +1266,34 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="InputData">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
-            <xsd:element name="x2" type="xsd:string" />
-            <xsd:element name="x3" type="xsd:string" />
+            <xsd:element name="x1" type="xsd:string"/>
+            <xsd:element name="x2" type="xsd:string"/>
+            <xsd:element name="x3" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:InputData" />
+    <wsdl:part name="parameters" element="ns:InputData"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1299,7 +1303,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_no_types():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1309,17 +1313,17 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     <xsd:schema targetNamespace="my-namespace"
     elementFormDefault="qualified"
     attributeFormDefault="unqualified"
-    xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"/>
   </wsdl:types>
   <wsdl:portType name="dummyPortType">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1335,56 +1339,60 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_parameter_referencing_missing_element(monkeypatch):
-    wsdl = tests.wsdl_input("", "missingElement")
+    wsdl = testutils.wsdl("", input="missingElement",
+        xsd_target_namespace="aaa")
     monkeypatch.delitem(locals(), "e", False)
-    e = pytest.raises(suds.TypeNotFound, tests.client_from_wsdl, wsdl).value
-    assert str(e) == "Type not found: '(missingElement, my-namespace, )'"
+    e = pytest.raises(suds.TypeNotFound, testutils.client_from_wsdl, wsdl)
+    try:
+        assert str(e.value) == "Type not found: '(missingElement, aaa, )'"
+    finally:
+        del e  # explicitly break circular reference chain in Python 3
 
 
-# TODO: Update the current restriction type input parameter handling so they get
+#TODO: Update the current restriction type input parameter handling so they get
 # 'unwrapped' correctly instead of each of their enumeration values getting
 # interpreted as a separate input parameter.
 @pytest.mark.xfail
 def test_restrictions():
-    client_unnamed = tests.client_from_wsdl(tests.wsdl_input("""\
+    client_unnamed = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:element name="Elemento">
         <xsd:simpleType>
           <xsd:restriction base="xsd:int">
-            <xsd:enumeration value="1" />
-            <xsd:enumeration value="3" />
-            <xsd:enumeration value="5" />
+            <xsd:enumeration value="1"/>
+            <xsd:enumeration value="3"/>
+            <xsd:enumeration value="5"/>
           </xsd:restriction>
         </xsd:simpleType>
-      </xsd:element>""", "Elemento"))
+      </xsd:element>""", input="Elemento"))
 
-    client_named = tests.client_from_wsdl(tests.wsdl_input("""\
+    client_named = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:simpleType name="MyType">
         <xsd:restriction base="xsd:int">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
-      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+      <xsd:element name="Elemento" type="ns:MyType"/>""", input="Elemento"))
 
-    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_input("""\
+    client_twice_restricted = testutils.client_from_wsdl(testutils.wsdl("""\
       <xsd:simpleType name="MyTypeGeneric">
         <xsd:restriction base="xsd:int">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="2" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="4" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="2"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="4"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
       <xsd:simpleType name="MyType">
         <xsd:restriction base="ns:MyTypeGeneric">
-          <xsd:enumeration value="1" />
-          <xsd:enumeration value="3" />
-          <xsd:enumeration value="5" />
+          <xsd:enumeration value="1"/>
+          <xsd:enumeration value="3"/>
+          <xsd:enumeration value="5"/>
         </xsd:restriction>
       </xsd:simpleType>
-      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+      <xsd:element name="Elemento" type="ns:MyType"/>""", input="Elemento"))
 
     element_qref = ("Elemento", "my-namespace")
     type_named_qref = ("MyType", "my-namespace")
@@ -1402,7 +1410,7 @@ def test_restrictions():
     assert type_twice_restricted is client_twice_restricted.wsdl.schema.types[
         type_named_qref]
 
-    #   Regression test against suds automatically unwrapping input parameter
+    # Regression test against suds automatically unwrapping input parameter
     # type's enumeration values as separate parameters.
     params_unnamed = client_unnamed.sd[0].params
     params_named = client_named.sd[0].params
@@ -1419,7 +1427,7 @@ def test_restrictions():
 
 
 def test_schema_node_occurrences():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1508,7 +1516,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_schema_node_resolve():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1521,28 +1529,28 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="Typo">
         <xsd:sequence>
-          <xsd:element name="u1" type="xsd:string" />
-          <xsd:element name="u2" type="xsd:string" />
-          <xsd:element name="u3" type="xsd:string" />
+          <xsd:element name="u1" type="xsd:string"/>
+          <xsd:element name="u2" type="xsd:string"/>
+          <xsd:element name="u3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
-            <xsd:element name="x2" type="Typo" />
+            <xsd:element name="x1" type="xsd:string"/>
+            <xsd:element name="x2" type="Typo"/>
             <xsd:element name="x3">
               <xsd:complexType>
                 <xsd:sequence>
-                  <xsd:element name="a1" type="xsd:string" />
-                  <xsd:element name="a2" type="xsd:string" />
+                  <xsd:element name="a1" type="xsd:string"/>
+                  <xsd:element name="a2" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
             </xsd:element>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
-      <xsd:element name="ElementoTyped" type="Typo" />
+      <xsd:element name="ElementoTyped" type="Typo"/>
     </xsd:schema>
   </wsdl:types>
 </wsdl:definitions>
@@ -1562,7 +1570,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert elemento_x2.name == "x2"
     elemento_x3 = elemento.children()[2][0]
     assert elemento_x3.name == "x3"
-    elementoTyped = schema.elements["ElementoTyped", "my-namespace"]
+    elemento_typed = schema.elements["ElementoTyped", "my-namespace"]
 
     # Resolving top-level locally defined non-content nodes.
     assert typo.resolve() is typo
@@ -1571,7 +1579,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert elemento.resolve() is elemento
 
     # Resolving top-level globally typed elements.
-    assert elementoTyped.resolve() is typo
+    assert elemento_typed.resolve() is typo
 
     # Resolving a subnode referencing a globally defined type.
     assert elemento_x2.resolve() is typo
@@ -1589,7 +1597,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_schema_node_resolve__nobuiltin_caching():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1600,10 +1608,10 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     elementFormDefault="qualified"
     attributeFormDefault="unqualified"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-      <xsd:element name="Elemento1" type="xsd:string" />
-      <xsd:element name="Elemento2" type="xsd:string" />
-      <xsd:element name="Elemento3" type="xsd:string" />
-      <xsd:element name="Elemento4" type="xsd:string" />
+      <xsd:element name="Elemento1" type="xsd:string"/>
+      <xsd:element name="Elemento2" type="xsd:string"/>
+      <xsd:element name="Elemento3" type="xsd:string"/>
+      <xsd:element name="Elemento4" type="xsd:string"/>
     </xsd:schema>
   </wsdl:types>
 </wsdl:definitions>
@@ -1617,7 +1625,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     e3 = schema.elements["Elemento3", "my-namespace"]
     e4 = schema.elements["Elemento4", "my-namespace"]
 
-    #   Repeating the same resolve() call twice makes sure that the first call
+    # Repeating the same resolve() call twice makes sure that the first call
     # does not cache an incorrect value, thus causing the second call to return
     # an incorrect result.
 
@@ -1637,7 +1645,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_schema_node_resolve__invalid_type():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1648,9 +1656,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     elementFormDefault="qualified"
     attributeFormDefault="unqualified"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-      <xsd:element name="Elemento1" type="Elemento1" />
-      <xsd:element name="Elemento2" type="Elemento1" />
-      <xsd:element name="Elemento3" type="XXX" />
+      <xsd:element name="Elemento1" type="Elemento1"/>
+      <xsd:element name="Elemento2" type="Elemento1"/>
+      <xsd:element name="Elemento3" type="XXX"/>
     </xsd:schema>
   </wsdl:types>
 </wsdl:definitions>
@@ -1666,7 +1674,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_schema_node_resolve__references():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1679,21 +1687,21 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="Typo">
         <xsd:sequence>
-          <xsd:element name="u1" type="xsd:string" />
-          <xsd:element name="u2" type="xsd:string" />
-          <xsd:element name="u3" type="xsd:string" />
+          <xsd:element name="u1" type="xsd:string"/>
+          <xsd:element name="u2" type="xsd:string"/>
+          <xsd:element name="u3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
-      <xsd:element name="ElementoTyped" type="Typo" />
-      <xsd:element name="ElementoTyped11" ref="ElementoTyped" />
-      <xsd:element name="ElementoTyped12" ref="ElementoTyped11" />
-      <xsd:element name="ElementoTyped13" ref="ElementoTyped12" />
-      <xsd:element name="ElementoTyped21" ref="ElementoTyped" />
-      <xsd:element name="ElementoTyped22" ref="ElementoTyped21" />
-      <xsd:element name="ElementoTyped23" ref="ElementoTyped22" />
-      <xsd:element name="ElementoTypedX" ref="ElementoTypedX" />
-      <xsd:element name="ElementoTypedX1" ref="ElementoTypedX2" />
-      <xsd:element name="ElementoTypedX2" ref="ElementoTypedX1" />
+      <xsd:element name="ElementoTyped" type="Typo"/>
+      <xsd:element name="ElementoTyped11" ref="ElementoTyped"/>
+      <xsd:element name="ElementoTyped12" ref="ElementoTyped11"/>
+      <xsd:element name="ElementoTyped13" ref="ElementoTyped12"/>
+      <xsd:element name="ElementoTyped21" ref="ElementoTyped"/>
+      <xsd:element name="ElementoTyped22" ref="ElementoTyped21"/>
+      <xsd:element name="ElementoTyped23" ref="ElementoTyped22"/>
+      <xsd:element name="ElementoTypedX" ref="ElementoTypedX"/>
+      <xsd:element name="ElementoTypedX1" ref="ElementoTypedX2"/>
+      <xsd:element name="ElementoTypedX2" ref="ElementoTypedX1"/>
     </xsd:schema>
   </wsdl:types>
 </wsdl:definitions>
@@ -1704,45 +1712,46 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert len(schema.types) == 1
     typo = schema.types["Typo", "my-namespace"]
     assert len(schema.elements) == 10
-    elementoTyped = schema.elements["ElementoTyped", "my-namespace"]
-    elementoTyped11 = schema.elements["ElementoTyped11", "my-namespace"]
-    elementoTyped12 = schema.elements["ElementoTyped12", "my-namespace"]
-    elementoTyped13 = schema.elements["ElementoTyped13", "my-namespace"]
-    elementoTyped21 = schema.elements["ElementoTyped21", "my-namespace"]
-    elementoTyped22 = schema.elements["ElementoTyped22", "my-namespace"]
-    elementoTyped23 = schema.elements["ElementoTyped23", "my-namespace"]
-    elementoTypedX = schema.elements["ElementoTypedX", "my-namespace"]
-    elementoTypedX1 = schema.elements["ElementoTypedX1", "my-namespace"]
-    elementoTypedX2 = schema.elements["ElementoTypedX2", "my-namespace"]
-
-    #   For referenced element node chains try resolving their nodes in both
+    elemento_typed = schema.elements["ElementoTyped", "my-namespace"]
+    elemento_typed11 = schema.elements["ElementoTyped11", "my-namespace"]
+    elemento_typed12 = schema.elements["ElementoTyped12", "my-namespace"]
+    elemento_typed13 = schema.elements["ElementoTyped13", "my-namespace"]
+    elemento_typed21 = schema.elements["ElementoTyped21", "my-namespace"]
+    elemento_typed22 = schema.elements["ElementoTyped22", "my-namespace"]
+    elemento_typed23 = schema.elements["ElementoTyped23", "my-namespace"]
+    elemento_typedX = schema.elements["ElementoTypedX", "my-namespace"]
+    elemento_typedX1 = schema.elements["ElementoTypedX1", "my-namespace"]
+    elemento_typedX2 = schema.elements["ElementoTypedX2", "my-namespace"]
+
+    # For referenced element node chains try resolving their nodes in both
     # directions and try resolving them twice to try and avoid any internal
-    # resolve result caching that might cause some resursive resolution branch
+    # resolve result caching that might cause some recursive resolution branch
     # to not get taken.
-    #   Note that these assertions are actually redundant since inter-element
+    #
+    # Note that these assertions are actually redundant since inter-element
     # references get processed and referenced type information merged back into
     # the referencee when the schema information is loaded so no recursion is
     # needed here in the first place. The tests should still be left in place
     # and pass to serve as a safeguard in case this reference processing gets
     # changed in the future.
-    assert elementoTyped11.resolve() is typo
-    assert elementoTyped11.resolve() is typo
-    assert elementoTyped13.resolve() is typo
-    assert elementoTyped13.resolve() is typo
+    assert elemento_typed11.resolve() is typo
+    assert elemento_typed11.resolve() is typo
+    assert elemento_typed13.resolve() is typo
+    assert elemento_typed13.resolve() is typo
 
-    assert elementoTyped23.resolve() is typo
-    assert elementoTyped23.resolve() is typo
-    assert elementoTyped21.resolve() is typo
-    assert elementoTyped21.resolve() is typo
+    assert elemento_typed23.resolve() is typo
+    assert elemento_typed23.resolve() is typo
+    assert elemento_typed21.resolve() is typo
+    assert elemento_typed21.resolve() is typo
 
     # Recursive element references.
-    assert elementoTypedX.resolve() is elementoTypedX
-    assert elementoTypedX1.resolve() is elementoTypedX1
-    assert elementoTypedX2.resolve() is elementoTypedX2
+    assert elemento_typedX.resolve() is elemento_typedX
+    assert elemento_typedX1.resolve() is elemento_typedX1
+    assert elemento_typedX2.resolve() is elemento_typedX2
 
 
 def test_schema_object_child_access_by_index():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1755,9 +1764,9 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="Oklahoma">
         <xsd:sequence>
-          <xsd:element name="c1" type="xsd:string" />
-          <xsd:element name="c2" type="xsd:string" />
-          <xsd:element name="c3" type="xsd:string" />
+          <xsd:element name="c1" type="xsd:string"/>
+          <xsd:element name="c2" type="xsd:string"/>
+          <xsd:element name="c3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
     </xsd:schema>
@@ -1766,34 +1775,34 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
 """))
 
     service = client.sd[0]
-    aType = service.types[0][0]
-    sequence = aType.rawchildren[0]
+    a_type = service.types[0][0]
+    sequence = a_type.rawchildren[0]
     assert isinstance(sequence, suds.xsd.sxbasic.Sequence)
-    children = aType.children()
+    children = a_type.children()
     assert isinstance(children, list)
 
     assert sequence[-1] is None
 
-    # TODO: Children are returned as a 2-tuple containing the child element and
+    #TODO: Children are returned as a 2-tuple containing the child element and
     # its ancestry (list of its parent elements). For some reason the ancestry
     # list is returned as a new list on every __getitem__() call and so can not
     # be compared using the 'is' operator. Also the children() function and
-    # accesing children by index does not seem to return ancestry lists of the
+    # accessing children by index does not seem to return ancestry lists of the
     # same depth. See whether this can be updated so we always get the same
     # ancestry list object.
-    # TODO: Add more detailed tests for the ancestry list structure.
-    # TODO: Add more detailed tests for the rawchildren list structure.
+    #TODO: Add more detailed tests for the ancestry list structure.
+    #TODO: Add more detailed tests for the rawchildren list structure.
 
     assert isinstance(sequence[0], tuple)
     assert len(sequence[0]) == 2
@@ -1811,7 +1820,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_simple_wsdl():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1825,44 +1834,44 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
       <xsd:element name="f">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="a" type="xsd:string" />
-            <xsd:element name="b" type="xsd:string" />
+            <xsd:element name="a" type="xsd:string"/>
+            <xsd:element name="b" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
       <xsd:element name="fResponse">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="c" type="xsd:string" />
+            <xsd:element name="c" type="xsd:string"/>
           </xsd:sequence>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
   </wsdl:types>
   <wsdl:message name="fRequestMessage">
-    <wsdl:part name="parameters" element="ns:f" />
+    <wsdl:part name="parameters" element="ns:f"/>
   </wsdl:message>
   <wsdl:message name="fResponseMessage">
-    <wsdl:part name="parameters" element="ns:fResponse" />
+    <wsdl:part name="parameters" element="ns:fResponse"/>
   </wsdl:message>
   <wsdl:portType name="dummyPortType">
     <wsdl:operation name="f">
-      <wsdl:input message="ns:fRequestMessage" />
-      <wsdl:output message="ns:fResponseMessage" />
+      <wsdl:input message="ns:fRequestMessage"/>
+      <wsdl:output message="ns:fResponseMessage"/>
     </wsdl:operation>
   </wsdl:portType>
   <wsdl:binding name="dummy" type="ns:dummyPortType">
     <soap:binding style="document"
-    transport="http://schemas.xmlsoap.org/soap/http" />
+    transport="http://schemas.xmlsoap.org/soap/http"/>
     <wsdl:operation name="f">
-      <soap:operation soapAction="f" style="document" />
-      <wsdl:input><soap:body use="literal" /></wsdl:input>
-      <wsdl:output><soap:body use="literal" /></wsdl:output>
+      <soap:operation soapAction="f" style="document"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+      <wsdl:output><soap:body use="literal"/></wsdl:output>
     </wsdl:operation>
   </wsdl:binding>
   <wsdl:service name="dummy">
     <wsdl:port name="dummy" binding="ns:dummy">
-      <soap:address location="https://localhost/dummy" />
+      <soap:address location="https://localhost/dummy"/>
     </wsdl:port>
   </wsdl:service>
 </wsdl:definitions>
@@ -1874,21 +1883,21 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
     # Elements.
     assert len(client.wsdl.schema.elements) == 2
-    elementIn = client.wsdl.schema.elements["f", "my-namespace"]
-    elementOut = client.wsdl.schema.elements["fResponse", "my-namespace"]
-    assert isinstance(elementIn, suds.xsd.sxbasic.Element)
-    assert isinstance(elementOut, suds.xsd.sxbasic.Element)
-    assert elementIn.name == "f"
-    assert elementOut.name == "fResponse"
-    assert len(elementIn.children()) == 2
-    param_in_1 = elementIn.children()[0][0]
-    param_in_2 = elementIn.children()[1][0]
+    element_in = client.wsdl.schema.elements["f", "my-namespace"]
+    element_out = client.wsdl.schema.elements["fResponse", "my-namespace"]
+    assert isinstance(element_in, suds.xsd.sxbasic.Element)
+    assert isinstance(element_out, suds.xsd.sxbasic.Element)
+    assert element_in.name == "f"
+    assert element_out.name == "fResponse"
+    assert len(element_in.children()) == 2
+    param_in_1 = element_in.children()[0][0]
+    param_in_2 = element_in.children()[1][0]
     assert param_in_1.name == "a"
     assert param_in_1.type == _string_type
     assert param_in_2.name == "b"
     assert param_in_2.type == _string_type
-    assert len(elementOut.children()) == 1
-    param_out_1 = elementOut.children()[0][0]
+    assert len(element_out.children()) == 1
+    param_out_1 = element_out.children()[0][0]
     assert param_out_1.name == "c"
     assert param_out_1.type == _string_type
 
@@ -1909,27 +1918,27 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     assert len(service_definition.ports[0]) == 2
     assert service_definition.ports[0][0] is port
 
-    # Methods (from wsdl).
+    # Methods (from WSDL).
     assert len(port.methods) == 1
     method = port.methods["f"]
     assert method.name == "f"
     assert method.location == "https://localhost/dummy"
 
-    # Operations (from wsdl).
+    # Operations (from WSDL).
     assert len(client.wsdl.bindings) == 1
     binding_qname, binding = _first_from_dict(client.wsdl.bindings)
     assert binding_qname == ("dummy", "my-namespace")
     assert binding.__class__ is suds.wsdl.Binding
     assert len(binding.operations) == 1
-    operation = binding.operations.values()[0]
+    operation = next(itervalues(binding.operations))
     input = operation.soap.input.body
     output = operation.soap.output.body
     assert len(input.parts) == 1
     assert len(output.parts) == 1
     input_element_qname = input.parts[0].element
     output_element_qname = output.parts[0].element
-    assert input_element_qname == elementIn.qname
-    assert output_element_qname == elementOut.qname
+    assert input_element_qname == element_in.qname
+    assert output_element_qname == element_out.qname
 
     # Methods (from service definition, for format specifications see the
     # suds.serviceDefinition.ServiceDefinition.addports() docstring).
@@ -1958,7 +1967,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 
 
 def test_wsdl_schema_content():
-    client = tests.client_from_wsdl(suds.byte_str("""\
+    client = testutils.client_from_wsdl(suds.byte_str("""\
 <?xml version='1.0' encoding='UTF-8'?>
 <wsdl:definitions targetNamespace="my-namespace"
 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
@@ -1971,26 +1980,26 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:complexType name="UngaBunga">
         <xsd:sequence>
-          <xsd:element name="u1" type="xsd:string" />
-          <xsd:element name="u2" type="xsd:string" />
-          <xsd:element name="u3" type="xsd:string" />
+          <xsd:element name="u1" type="xsd:string"/>
+          <xsd:element name="u2" type="xsd:string"/>
+          <xsd:element name="u3" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
       <xsd:complexType name="Fifi">
         <xsd:sequence>
-          <xsd:element name="x" type="xsd:string" />
+          <xsd:element name="x" type="xsd:string"/>
         </xsd:sequence>
       </xsd:complexType>
       <xsd:element name="Elemento">
         <xsd:complexType>
           <xsd:sequence>
-            <xsd:element name="x1" type="xsd:string" />
-            <xsd:element name="x2" type="UngaBunga" />
+            <xsd:element name="x1" type="xsd:string"/>
+            <xsd:element name="x2" type="UngaBunga"/>
             <xsd:element name="x3">
               <xsd:complexType>
                 <xsd:sequence>
-                  <xsd:element name="a1" type="xsd:string" />
-                  <xsd:element name="a2" type="xsd:string" />
+                  <xsd:element name="a1" type="xsd:string"/>
+                  <xsd:element name="a2" type="xsd:string"/>
                 </xsd:sequence>
               </xsd:complexType>
             </xsd:element>
@@ -2024,7 +2033,7 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
 def _assert_dynamic_type(anObject, typename):
     assert anObject.__module__ == suds.sudsobject.__name__
     assert anObject.__metadata__.sxtype.name == typename
-    #   In order to be compatible with old style classes (py2 only) we need to
+    # In order to be compatible with old style classes (py2 only) we need to
     # access the object's class information using its __class__ member and not
     # the type() function. type() function always returns <type 'instance'> for
     # old-style class instances while the __class__ member returns the correct
@@ -2035,9 +2044,9 @@ def _assert_dynamic_type(anObject, typename):
 
 def _construct_SOAP_request(client, operation_name, *args, **kwargs):
     """
-    Returns a SOAP request for a given web service operation invocation.
+    Construct a SOAP request for a given web service operation invocation.
 
-      To make the test case code calling this function simpler, assumes we want
+    To make the test case code calling this function simpler, assumes we want
     to call the operation on the given client's first service & port.
 
     """
@@ -2054,8 +2063,8 @@ def _element_node_xml(name, min=None, max=None):
         s.append('minOccurs="%s" ' % (min,))
     if max is not None:
         s.append('maxOccurs="%s" ' % (max,))
-    s.append('/>\n')
-    return ''.join(s)
+    s.append("/>\n")
+    return "".join(s)
 
 
 def _first_from_dict(d):
diff --git a/tests/test_timezone.py b/tests/test_timezone.py
index c25db01..81c0df8 100644
--- a/tests/test_timezone.py
+++ b/tests/test_timezone.py
@@ -1,4 +1,6 @@
-# This program is free software; you can redistribute it and/or modify it under
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
 # the terms of the (LGPL) GNU Lesser General Public License as published by the
 # Free Software Foundation; either version 3 of the License, or (at your
 # option) any later version.
@@ -21,12 +23,10 @@ Implemented using the 'pytest' testing framework.
 """
 
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    import testutils
+    testutils.run_using_pytest(globals())
 
 from suds.sax.date import FixedOffsetTimezone, UtcTimezone
-import tests
 
 import pytest
 
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 8cd0454..d045aaf 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -23,80 +23,180 @@ Implemented using the 'pytest' testing framework.
 """
 
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    import testutils
+    testutils.run_using_pytest(globals())
 
 import suds
-from suds.transport import Reply, Request
+from suds.transport import Reply, Request, Transport
+import suds.transport.options
 
 import pytest
+from six import b, text_type, u, unichr
 
 import sys
 
 
- at pytest.mark.parametrize("message", (
-    u"",
-    u"for a bitch it's haaaard...",
-    u"I'm here to kick ass,\nand chew bubble gum...\nand I'm all out of gum.",
-    u"šuć-muć pa ožeži.. za 100 €\n\nwith multiple\nlines...",
-    u"\n\n\n\n\n\n",
-    u"中原千军逐蒋"))
-def test_reply_as_string(message):
-    code = 17
-    reply = Reply(code, {"aaa":1}, message)
-    expected = u"""\
+class TestBaseTransportClass:
+
+    def test_members(self):
+        t = Transport()
+        assert t.options.__class__ is suds.transport.options.Options
+
+    @pytest.mark.parametrize("method_name", ("open", "send"))
+    def test_methods_should_be_abstract(self, monkeypatch, method_name):
+        monkeypatch.delitem(locals(), "e", False)
+        transport = Transport()
+        f = getattr(transport, method_name)
+        e = pytest.raises(Exception, f, "whatever").value
+        try:
+            assert e.__class__ is Exception
+            assert str(e) == "not-implemented"
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestReply:
+
+    @pytest.mark.parametrize(("code", "headers", "message"), (
+        (1, {}, None),
+        (1, {}, u("ola")),
+        (1, {}, b("ola")),
+        (1, {}, object()),
+        (1, {}, u("\u0161u\u0107-mu\u0107 \u4E2D\u539F\u5343\n\u57CE")),
+        (2, {"semper": "fi"}, u("\u4E2D\u539F\u5343\n\u57CE"))))
+    def test_construction(self, code, headers, message):
+        reply = Reply(code, headers, message)
+        assert reply.code is code
+        assert reply.headers is headers
+        assert reply.message is message
+
+    @pytest.mark.parametrize("message", [u(x).encode("utf-8") for x in (
+        "",
+        "for a bitch it's haaaard...",
+        """\
+I'm here to kick ass,
+and chew bubble gum...
+and I'm all out of gum.""",
+        "\u0161u\u0107-mu\u0107 pa o\u017Ee\u017Ei.. za 100 \u20AC\n\n"
+            "with multiple\nlines...",
+        "\n\n\n\n\n\n",
+        "\u4E2D\u539F\u5343\u519B\u9010\u848B")])
+    def test_string_representation(self, message):
+        code = 17
+        reply = Reply(code, {"aaa": 1}, message)
+        expected = u("""\
 CODE: %s
 HEADERS: %s
 MESSAGE:
-%s""" % (code, reply.headers, message)
-    assert unicode(reply) == expected
-    if sys.version_info < (3, 0):
-        assert str(reply) == expected.encode("utf-8")
-
-
- at pytest.mark.parametrize(("code", "headers", "message"), (
-    (1, {}, "ola"),
-    (2, {"semper":"fi"}, u"中原千军逐蒋\n城楼万众检阅")))
-def test_reply_constructor(code, headers, message):
-    reply = Reply(code, headers, message)
-    assert reply.code == code
-    assert reply.headers == headers
-    assert reply.message == message
-
-
- at pytest.mark.parametrize("message", (
-    u"",
-    u"for a bitch it's haaaard...",
-    u"I'm here to kick ass,\nand chew bubble gum...\nand I'm all out of gum.",
-    u"šuć-muć pa ožeži.. za 100 €\n\nwith multiple\nlines...",
-    u"\n\n\n\n\n\n",
-    u"中原千军逐蒋"))
-def test_request_as_string(message):
-    request = Request("my url", message)
-    request.headers["aaa"] = 1
-    expected = u"""\
-URL: my url
+%s""") % (code, reply.headers, message.decode("raw_unicode_escape"))
+        assert text_type(reply) == expected
+        if sys.version_info < (3,):
+            assert str(reply) == expected.encode("utf-8")
+
+
+class TestRequest:
+
+    @pytest.mark.parametrize("message", (
+        None,
+        "it's hard out here...",
+        u("\u57CE\u697C\u4E07\u4F17\u68C0\u9605")))
+    def test_construct(self, message):
+        # Always use the same URL as different ways to specify a Request's URL
+        # are tested separately.
+        url = "some://url"
+        request = Request(url, message)
+        assert request.url is url
+        assert request.message is message
+        assert request.headers == {}
+
+    def test_construct_with_no_message(self):
+        request = Request("some://url")
+        assert request.headers == {}
+        assert request.message is None
+
+    test_non_ASCII_URLs = [
+        u("\u4E2D\u539F\u5343\u519B\u9010\u848B"),
+        u("\u57CE\u697C\u4E07\u4F17\u68C0\u9605")] + [
+        url_prefix + url_suffix
+            for url_prefix in (u(""), u("Jurko"))
+            for url_suffix in (unichr(128), unichr(200), unichr(1000))]
+    @pytest.mark.parametrize("url",
+        test_non_ASCII_URLs +  # unicode strings
+        [x.encode("utf-8") for x in test_non_ASCII_URLs])  # byte strings
+    def test_non_ASCII_URL(self, url):
+        """Transport Request should reject URLs with non-ASCII characters."""
+        pytest.raises(UnicodeError, Request, url)
+
+    @pytest.mark.parametrize(("url", "headers", "message"), (
+        ("my URL", {}, ""),
+        ("", {"aaa": "uf-uf"}, "for a bitch it's haaaard..."),
+        ("http://rumple-fif/muka-laka-hiki", {"uno": "eins", "zwei": "due"},
+            """\
+I'm here to kick ass,
+and chew bubble gum...
+and I'm all out of gum."""),
+        ("", {}, u("\u0161u\u0107-mu\u0107 pa o\u017Ee\u017Ei.. za 100 "
+            "\u20AC\n\nwith multiple\nlines...")),
+        ("", {}, "\n\n\n\n\n\n"),
+        ("", {}, u("\u4E2D\u539F\u5343\u519B\u9010\u848B"))))
+    def test_string_representation_with_message(self, url, headers, message):
+        for key, value in headers.items():
+            old_key = key
+            if isinstance(key, text_type):
+                key = key.encode("utf-8")
+                del headers[old_key]
+            if isinstance(value, text_type):
+                value = value.encode("utf-8")
+            headers[key] = value
+        if isinstance(message, text_type):
+            message = message.encode("utf-8")
+        request = Request(url, message)
+        request.headers = headers
+        expected = u("""\
+URL: %s
 HEADERS: %s
 MESSAGE:
-%s""" % (request.headers, message)
-    assert unicode(request) == expected
-    if sys.version_info < (3, 0):
-        assert str(request) == expected.encode("utf-8")
-
-
- at pytest.mark.parametrize(("url", "message"), (
-    ("for a bitch it's haaaard...", "it's hard out here..."),
-    (u"中原千军逐蒋", u"城楼万众检阅")))
-def test_request_constructor(url, message):
-    request = Request(url, message)
-    assert request.url == url
-    assert request.message == message
-    assert request.headers == {}
-
-
-def test_request_without_message():
-    request = Request("for a bitch it's haaaard...")
-    assert request.url == "for a bitch it's haaaard..."
-    assert request.message is None
-    assert request.headers == {}
+%s""") % (url, request.headers, message.decode("raw_unicode_escape"))
+        assert text_type(request) == expected
+        if sys.version_info < (3,):
+            assert str(request) == expected.encode("utf-8")
+
+    def test_string_representation_with_no_message(self):
+        url = "look at my silly little URL"
+        headers = {suds.byte_str("yuck"): suds.byte_str("ptooiii...")}
+        request = Request(url)
+        request.headers = headers
+        expected = u("""\
+URL: %s
+HEADERS: %s""") % (url, request.headers)
+        assert text_type(request) == expected
+        if sys.version_info < (3,):
+            assert str(request) == expected.encode("utf-8")
+
+    test_URLs = [
+        u(""),
+        u("http://host/path/name"),
+        u("cogito://ergo/sum"),
+        u("haleluya"),
+        u("look  at  me flyyyyyyyy"),
+        unichr(0),
+        unichr(127),
+        u("Jurko") + unichr(0),
+        u("Jurko") + unichr(127)]
+    @pytest.mark.parametrize("url", test_URLs + [
+        url.encode("ascii") for url in test_URLs])
+    def test_URL(self, url):
+        """
+        Transport Request accepts its URL as either a byte or a unicode string.
+
+        Internally URL information is kept as the native Python str type.
+
+        """
+        request = Request(url)
+        assert isinstance(request.url, str)
+        if url.__class__ is str:
+            assert request.url is url
+        elif url.__class__ is u:
+            assert request.url == url.encode("ascii")  # Python 2.
+        else:
+            assert request.url == url.decode("ascii")  # Python 3.
diff --git a/tests/test_transport_http.py b/tests/test_transport_http.py
index 1502b0d..72cf160 100644
--- a/tests/test_transport_http.py
+++ b/tests/test_transport_http.py
@@ -22,21 +22,36 @@ Implemented using the 'pytest' testing framework.
 
 """
 
+import testutils
 if __name__ == "__main__":
-    import __init__
-    __init__.runUsingPyTest(globals())
-
+    testutils.run_using_pytest(globals())
 
 import suds
-import suds.client
-import suds.store
+import suds.transport
 import suds.transport.http
 
 import pytest
+from six import u
+from six.moves import http_client
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.request import ProxyHandler
 
 import base64
+import re
 import sys
-import urllib2
+
+# We can not use six.moves modules for this since we want to monkey-patch the
+# exact underlying urllib2/urllib.request module in our tests and not just
+# their six.moves proxy module.
+if sys.version_info < (3,):
+    import urllib2 as urllib_request
+else:
+    import urllib.request as urllib_request
+
+
+class MustNotBeCalled(Exception):
+    """Local exception used in this test module."""
+    pass
 
 
 class MyException(Exception):
@@ -44,355 +59,557 @@ class MyException(Exception):
     pass
 
 
-def test_authenticated_http():
-    t = suds.transport.http.HttpAuthenticated(username="Habul AfuFa",
-        password="preCious")
-    assert t.credentials() == ("Habul AfuFa", "preCious")
+class Undefined:
+    """Internal tag class indicating undefined function call parameters."""
+    pass
 
-    t = suds.transport.http.HttpAuthenticated(username="macro")
-    assert t.credentials() == ("macro", None)
 
+class CountedMock(object):
+    """
+    Base mock object class supporting generic attribute access counting.
 
-def test_authenticated_http_add_credentials_to_request():
-    class MockRequest:
-        def __init__(self):
-            self.headers = {}
+    Ignores attributes whose name starts with 'mock_' or '__mock_' or their
+    transformed variant '_<className>__mock_'.
 
-    t = suds.transport.http.HttpAuthenticated(username="Humpty")
-    r = MockRequest()
-    t.addcredentials(r)
-    assert len(r.headers) == 0
+    Derived classes must call this class's __init__() or mock_reset() methods
+    during their initialization, but both calls are not needed. Before this
+    initialization, all counters will be reported as None.
 
-    t = suds.transport.http.HttpAuthenticated(password="Dumpty")
-    r = MockRequest()
-    t.addcredentials(r)
-    assert len(r.headers) == 0
+    """
 
-    username = "Habul Afufa"
-    password = "preCious"
-    t = suds.transport.http.HttpAuthenticated(username=username,
-        password=password)
-    r = MockRequest()
-    t.addcredentials(r)
-    _check_Authorization_header(r, username, password)
-
-    #   Regression test: Extremely long username & password combinations must
-    # not cause suds to add additional newlines in the constructed
-    # 'Authorization' HTTP header.
-    username = ("An Extremely Long Username that could be usable only to "
-        "Extremely Important People whilst on Extremely Important Missions.")
-    password = ("An Extremely Long Password that could be usable only to "
-        "Extremely Important People whilst on Extremely Important Missions. "
-        "And some extra 'funny' characters to improve security: "
-        "!@#$%^&*():|}|{{.\nEven\nSome\nNewLines\n"
-        "  and spaces at the start of a new line.   ")
-    t = suds.transport.http.HttpAuthenticated(username=username,
-        password=password)
-    r = MockRequest()
-    t.addcredentials(r)
-    _check_Authorization_header(r, username, password)
+    def __init__(self):
+        self.mock_reset()
+
+    def __getattribute__(self, name):
+        get = super(CountedMock, self).__getattribute__
+        counter_name = "_CountedMock__mock_call_counter"
+        has_counter = False
+        try:
+            counter = get(counter_name)
+            has_counter = True
+        except AttributeError:
+            pass
+        if has_counter:
+            if name == counter_name:
+                return counter
+            if not re.match("(_.+__)?mock_", name):
+                counter[name] = counter.get(name, 0) + 1
+        return get(name)
+
+    def mock_call_count(self, name):
+        if self.__mock_call_counter:
+            return self.__mock_call_counter.get(name, 0)
+
+    def mock_reset(self):
+        self.__mock_call_counter = {}
+
+
+class MockFP:
+    """
+    Mock FP 'File object' as stored inside Python's HTTPError exception.
+
+    Must have several 'File object' methods defined on it as Python's HTTPError
+    implementation expects them and stores references to them internally, at
+    least with Python 2.4.
+
+    """
+
+    def read():
+        raise MustNotBeCalled
+
+    def readline():
+        raise MustNotBeCalled
+
+
+class MockURLOpenerSaboteur:
+    """
+    Mock URLOpener raising an exception in its open() method.
+
+    If no open_exception is given in its initializer, simply marks the current
+    test as a failure if its open() method is called. Otherwise raises the
+    given exception from that call.
+
+    """
+
+    def __init__(self, open_exception=None):
+        self.__open_exception = open_exception
+
+    def open(self, *args, **kwargs):
+        if self.__open_exception:
+            raise self.__open_exception
+        pytest.fail("urllib urlopener.open() must not be called.")
+
+
+class SendMethodFixture:
+    """
+    Instances of this class get returned by the send_method test fixture.
+
+    Each instance is connected to a specific suds.transport.http.HttpTransport
+    request sending method and may be used to call that method on a specific
+    suds.transport.http.HttpTransport instance.
+
+    """
 
+    def __init__(self, name):
+        self.name = name
 
- at pytest.mark.parametrize("url", (
+    def __call__(self, transport, *args, **kwargs):
+        assert isinstance(transport, suds.transport.http.HttpTransport)
+        return getattr(transport, self.name)(*args, **kwargs)
+
+
+# Test URL data used by several tests in this test module.
+test_URL_data = (
+    "sudo://make-me-a-sammich",
     "http://my little URL",
     "https://my little URL",
     "xxx://my little URL",
     "xxx:my little URL",
-    "xxx:"))
-def test_http_request_URL(url):
-    """Make sure suds makes a HTTP request targeted at an expected URL."""
-    class MockURLOpener:
-        def open(self, request, timeout=None):
-            assert request.get_full_url() == url
-            raise MyException
-    transport = suds.transport.http.HttpTransport()
-    transport.urlopener = MockURLOpener()
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
-        transport=transport)
-    pytest.raises(MyException, client.service.f)
+    "xxx:")
 
 
- at pytest.mark.parametrize("url", (
-    "my no-protocol URL",
-    ":my no-protocol URL"))
-def test_http_request_URL_with_a_missing_protocol_identifier(url):
-    """
-    Test suds reporting URLs with a missing protocol identifier.
+def assert_default_transport(transport):
+    """Test utility verifying default constructed transport content."""
+    assert isinstance(transport, suds.transport.http.HttpTransport)
+    assert transport.urlopener is None
+
+
+def create_request(url="protocol://default-url", data=u("Rumpelstiltskin")):
+    """Test utility constructing a suds.transport.Request instance."""
+    return suds.transport.Request(url, data)
 
-    Python urllib library makes this check under Python 3.x, but does not under
-    earlier Python versions.
 
+ at pytest.fixture(params=["open", "send"])
+def send_method(request):
     """
-    class MockURLOpener:
-        def open(self, request, timeout=None):
-            raise MyException
+    pytest testing framework based test fixture causing tests using that
+    fixture to be called for each suds.transport.http.HttpTransport request
+    sending method.
+
+    """
+    return SendMethodFixture(request.param)
+
+
+ at pytest.mark.parametrize("input", (
+    dict(),
+    dict(password="Humpty"),
+    dict(username="Dumpty"),
+    dict(username="Habul Afufa", password="preCious"),
+    # Regression test: Extremely long username & password combinations must not
+    # cause suds to add additional newlines in the constructed 'Authorization'
+    # HTTP header.
+    dict(username="An Extremely Long Username that could be usable only to "
+        "Extremely Important People whilst on Extremely Important Missions.",
+        password="An Extremely Long Password that could be usable only to "
+        "Extremely Important People whilst on Extremely Important Missions. "
+        "And some extra 'funny' characters to improve security: "
+        "!@#$%^&*():|}|{{.\nEven\nSome\nNewLines\n"
+        "  and spaces at the start of a new line.   ")))
+def test_authenticated_http_add_credentials_to_request(input):
+    class MockRequest:
+        def __init__(self):
+            self.headers = {}
+
+    def assert_Authorization_header(request, username, password):
+        if username is None or password is None:
+            assert len(request.headers) == 0
+        else:
+            assert len(request.headers) == 1
+            header = request.headers["Authorization"]
+            assert header == _encode_basic_credentials(username, password)
+
+    username = input.get("username", None)
+    password = input.get("password", None)
+    t = suds.transport.http.HttpAuthenticated(**input)
+    r = MockRequest()
+    t.addcredentials(r)
+    assert_Authorization_header(r, username, password)
+
+
+ at pytest.mark.parametrize("input", (
+    dict(password="riff raff..!@#"),
+    dict(username="macro"),
+    dict(username="Hab AfuFa", password="preCious")))
+def test_construct_authenticated_http(input):
+    expected_username = input.get("username", None)
+    expected_password = input.get("password", None)
+    transport = suds.transport.http.HttpAuthenticated(**input)
+    assert transport.credentials() == (expected_username, expected_password)
+    assert_default_transport(transport)
+
+
+def test_construct_http():
     transport = suds.transport.http.HttpTransport()
-    transport.urlopener = MockURLOpener()
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
-        transport=transport)
-    exceptionClass = ValueError
-    if sys.version_info < (3, 0):
-        exceptionClass = MyException
-    pytest.raises(exceptionClass, client.service.f)
+    assert_default_transport(transport)
 
 
-def test_sending_unicode_data(monkeypatch):
+def test_sending_using_network_sockets(send_method, monkeypatch):
     """
-    Original suds implementation passed its request location URL to the
-    underlying HTTP request object as a unicode string.
-
-    Under Python 2.4 this causes no problems as that implementation simply
-    sends all the request data over the network as-is (and treats all unicode
-    data as bytes anyway).
-
-    Under Python 2.7 this causes the httplib HTTP request implementation to
-    convert all of its data to unicode, and do so by simply assuming that data
-    contains only ASCII characters. If any other characters are encountered, it
-    fails with an exception like "UnicodeDecodeError: 'ascii' codec can't
-    decode byte 0xd0 in position 290: ordinal not in range(128)".
-
-    Under Python 3.x the httplib HTTP request implementation automatically
-    converts its received URL to a bytes object (assuming it contains only
-    ASCII characters), thus avoiding the need to convert all the other request
-    data.
-
-    In order to trigger the problematic httplib behaviour we need to make suds
-    attempt to send a HTTP request over the network. On the other hand, we want
-    this test to work even on computers not connected to a network so we
-    monkey-patch the underlying network socket APIs, log all the data suds
-    attempt to send over the network and consider the test run successful once
-    suds attempt to read back data from the network.
+    Test that telling HttpTransport to send a request actually causes it to
+    send the expected data over the network.
+
+    In order to test this we need to make suds attempt to send a HTTP request
+    over the network. On the other hand, we want the test to work even on
+    computers not connected to a network so we monkey-patch the underlying
+    network socket APIs, log all the data suds attempts to send over the
+    network and consider the test run successful once suds attempts to read
+    back data from the network.
 
     """
-    def callOnce(f):
-        """Method decorator making sure its function only gets called once."""
-        def wrapper(self, *args, **kwargs):
-            fTag = "_%s__%s_called" % (self.__class__.__name__, f.__name__)
-            assert not hasattr(self, fTag)
-            setattr(self, fTag, True)
-            return f(self, *args, **kwargs)
-        return wrapper
-
-    class Mocker:
-        def __init__(self, expectedHost, expectedPort):
-            self.expectedHost = expectedHost
-            self.expectedPort = expectedPort
-            self.sentData = suds.byte_str()
-            self.hostAddress = object()
-        @callOnce
+
+    class Mocker(CountedMock):
+        def __init__(self, expected_host, expected_port):
+            self.expected_host = expected_host
+            self.expected_port = expected_port
+            self.host_address = object()
+            self.mock_reset()
         def getaddrinfo(self, host, port, *args, **kwargs):
-            assert host == self.expectedHost
-            assert port == self.expectedPort
-            return [(None, None, None, None, self.hostAddress)]
-        @callOnce
+            assert host == self.expected_host
+            assert port == self.expected_port
+            return [(None, None, None, None, self.host_address)]
+        def mock_reset(self):
+            super(Mocker, self).mock_reset()
+            self.mock_sent_data = suds.byte_str()
+            self.mock_socket = None
         def socket(self, *args, **kwargs):
-            self.socket = MockSocket(self)
-            return self.socket
+            assert self.mock_socket is None
+            self.mock_socket = MockSocket(self)
+            return self.mock_socket
 
-    class MockSocketReader:
-        @callOnce
-        def readline(self, *args, **kwargs):
-            raise MyException
-
-    class MockSocket:
+    class MockSocket(CountedMock):
         def __init__(self, mocker):
             self.__mocker = mocker
-        @callOnce
+            self.mock_reset()
         def connect(self, address):
-            assert address is self.__mocker.hostAddress
-        @callOnce
+            assert address is self.__mocker.host_address
         def makefile(self, *args, **kwargs):
-            return MockSocketReader()
+            assert self.mock_reader is None
+            self.mock_reader = MockSocketReader()
+            return self.mock_reader
+        def mock_reset(self):
+            super(MockSocket, self).mock_reset()
+            self.mock_reader = None
         def sendall(self, data):
-            # Python 2.4 urllib implementation calls this function twice - once
-            # for sending the HTTP request headers and once for its body.
-            self.__mocker.sentData += data
-        @callOnce
+            self.__mocker.mock_sent_data += data
         def settimeout(self, *args, **kwargs):
-            assert not hasattr(self, "settimeout_called")
-            self.settimeout_called = True
+            pass
+
+    class MockSocketReader(CountedMock):
+        def __init__(self):
+            super(MockSocketReader, self).__init__()
+        def readline(self, *args, **kwargs):
+            raise MyException
 
+    # Setup.
     host = "an-easily-recognizable-host-name-214894932"
     port = 9999
+    host_port = "%s:%s" % (host, port)
+    url_relative = "svc"
+    url = "http://%s/%s" % (host_port, url_relative)
+    partial_ascii_byte_data = suds.byte_str("Muka-laka-hiki")
+    non_ascii_byte_data = u("\u0414\u043C\u0438 \u0442\u0440").encode("utf-8")
+    non_ascii_byte_data += partial_ascii_byte_data
     mocker = Mocker(host, port)
     monkeypatch.setattr("socket.getaddrinfo", mocker.getaddrinfo)
     monkeypatch.setattr("socket.socket", mocker.socket)
-    url = "http://%s:%s/svc" % (host, port)
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_input_data(url))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store)
-    data = u"Дмитровский район"
-    pytest.raises(MyException, client.service.f, data)
-    assert data.encode("utf-8") in mocker.sentData
-
-
-def test_sending_non_ascii_location():
-    """
-    Suds should refuse to send HTTP requests with a target location string
-    containing non-ASCII characters. URLs are supposed to consist of
-    characters only.
-
-    """
-    class MockURLOpener:
-        def open(self, request, timeout=None):
-            raise MyException
-    url = u"http://Дмитровский-район-152312306:9999/svc"
+    request = suds.transport.Request(url, non_ascii_byte_data)
     transport = suds.transport.http.HttpTransport()
-    transport.urlopener = MockURLOpener()
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
-        transport=transport)
-    pytest.raises(UnicodeEncodeError, client.service.f)
-
-
- at pytest.mark.skipif(sys.version_info >= (3, 0),
-    reason="Python 2 specific functionality")
- at pytest.mark.parametrize(("urlString", "expectedException"), (
-    ("http://jorgula", MyException),
-    ("http://jorgula_\xe7", UnicodeDecodeError)))
-def test_sending_py2_bytes_location(urlString, expectedException):
+    expected_sent_data_start, expected_request_data_send = {
+        "open": ("GET", False),
+        "send": ("POST", True)}[send_method.name]
+
+    # Execute.
+    pytest.raises(MyException, send_method, transport, request)
+
+    # Verify.
+    assert mocker.mock_call_count("getaddrinfo") == 1
+    assert mocker.mock_call_count("socket") == 1
+    assert mocker.mock_socket.mock_call_count("connect") == 1
+    assert mocker.mock_socket.mock_call_count("makefile") == 1
+    # With older Python versions, e.g. Python 2.4, urllib implementation calls
+    # Socket's sendall() method twice - once for sending the HTTP request
+    # headers and once for its body.
+    assert mocker.mock_socket.mock_call_count("sendall") in (1, 2)
+    # With older Python versions, e.g. Python 2.4, Socket class does not
+    # implement the settimeout() method.
+    assert mocker.mock_socket.mock_call_count("settimeout") in (0, 1)
+    assert mocker.mock_socket.mock_reader.mock_call_count("readline") == 1
+    assert mocker.mock_sent_data.__class__ is suds.byte_str_class
+    expected_sent_data_start = "%s /%s HTTP/1.1\r\n" % (
+        expected_sent_data_start, url_relative)
+    expected_sent_data_start = suds.byte_str(expected_sent_data_start)
+    assert mocker.mock_sent_data.startswith(expected_sent_data_start)
+    assert host_port.encode("utf-8") in mocker.mock_sent_data
+    if expected_request_data_send:
+        assert mocker.mock_sent_data.endswith(non_ascii_byte_data)
+    else:
+        assert partial_ascii_byte_data not in mocker.mock_sent_data
+
+
+class TestSendingToURLWithAMissingProtocolIdentifier:
     """
-    Suds should accept single-byte string URL values under Python 2, but should
-    still report an error if those strings contain any non-ASCII characters.
+    Test suds reporting URLs with a missing protocol identifier.
 
-    """
-    class MockURLOpener:
-        def open(self, request, timeout=None):
-            raise MyException
-    transport = suds.transport.http.HttpTransport()
-    transport.urlopener = MockURLOpener()
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data("http://x"))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
-        transport=transport)
-    client.options.location = suds.byte_str(urlString)
-    pytest.raises(expectedException, client.service.f)
-
-
- at pytest.mark.skipif(sys.version_info < (3, 0),
-    reason="requires at least Python 3")
- at pytest.mark.parametrize("urlString", (
-    "http://jorgula",
-    "http://jorgula_\xe7"))
-def test_sending_py3_bytes_location(urlString):
-    """
-    Suds should refuse to send HTTP requests with a target location specified
-    as either a Python 3 bytes or bytearray object.
+    Python urllib library makes this check under Python 3.x, but not under
+    earlier Python versions.
 
     """
-    class MockURLOpener:
-        def open(self, request, timeout=None):
-            raise MyException
-    transport = suds.transport.http.HttpTransport()
-    transport.urlopener = MockURLOpener()
-    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data("http://x"))
-    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
-        transport=transport)
 
-    expectedException = AssertionError
-    if sys.flags.optimize:
-        expectedException = AttributeError
+    # We can not set this 'url' fixture data using a class decorator since that
+    # Python feature has been introduced in Python 2.6 and we need to keep this
+    # code backward compatible with Python 2.4.
+    invalid_URL_parametrization = pytest.mark.parametrize("url", (
+        "my no-protocol URL",
+        ":my no-protocol URL"))
+
+    @pytest.mark.skipif(sys.version_info >= (3,), reason="Python 2 specific")
+    @invalid_URL_parametrization
+    def test_python2(self, url, send_method):
+        transport = suds.transport.http.HttpTransport()
+        transport.urlopener = MockURLOpenerSaboteur(MyException)
+        request = create_request(url)
+        pytest.raises(MyException, send_method, transport, request)
+
+    @pytest.mark.skipif(sys.version_info < (3,), reason="Python 3+ specific")
+    @invalid_URL_parametrization
+    def test_python3(self, url, send_method, monkeypatch):
+        monkeypatch.delitem(locals(), "e", False)
+        transport = suds.transport.http.HttpTransport()
+        transport.urlopener = MockURLOpenerSaboteur()
+        request = create_request(url)
+        e = pytest.raises(ValueError, send_method, transport, request)
+        try:
+            assert "unknown url type" in str(e)
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestURLOpenerUsage:
+    """
+    Test demonstrating how suds.transport.http.HttpTransport makes use of the
+    urllib library to perform the actual network transfers.
 
-    for url in (bytes(urlString, encoding="utf-8"),
-        bytearray(urlString, encoding="utf-8")):
-        # Under Python 3.x we can not use the client's 'location' option to set
-        # a bytes URL as it accepts only strings and in Python 3.x all strings
-        # are unicode strings. Therefore, we use an ugly hack, modifying suds's
-        # internal web service description structure to force it to think it
-        # has a bytes object specified as a location for its 'f' web service
-        # operation.
-        client.sd[0].ports[0][0].methods['f'].location = url
-        pytest.raises(expectedException, client.service.f)
+    The main contact point with the urllib library are its OpenerDirector
+    objects we refer to as 'urlopener'.
 
+    """
 
-def _check_Authorization_header(request, username, password):
-    assert len(request.headers) == 1
-    header = request.headers["Authorization"]
-    assert header == _encode_basic_credentials(username, password)
+    @staticmethod
+    def create_HTTPError(url=Undefined, code=Undefined, msg=Undefined,
+            hdrs=Undefined, fp=None):
+        """
+        Test utility method constructing a HTTPError instance. Allows callers
+        to construct a HTTPError instance using input data they are interested
+        in, with some built-in default values used for any input data they are
+        not interested in.
+
+        """
+        if url is Undefined:
+            url = object()
+        if code is Undefined:
+            code = object()
+        if msg is Undefined:
+            msg = object()
+        if hdrs is Undefined:
+            hdrs = object()
+        return HTTPError(url=url, code=code, msg=msg, hdrs=hdrs, fp=fp)
+
+    @pytest.mark.parametrize("status_code", (
+        http_client.ACCEPTED,
+        http_client.NO_CONTENT,
+        http_client.RESET_CONTENT,
+        http_client.MOVED_PERMANENTLY,
+        http_client.BAD_REQUEST,
+        http_client.PAYMENT_REQUIRED,
+        http_client.FORBIDDEN,
+        http_client.NOT_FOUND,
+        http_client.INTERNAL_SERVER_ERROR,
+        http_client.NOT_IMPLEMENTED,
+        http_client.HTTP_VERSION_NOT_SUPPORTED))
+    def test_open_propagating_HTTPError_exceptions(self, status_code,
+            monkeypatch):
+        """
+        HttpTransport open() operation should transform HTTPError urlopener
+        exceptions to suds.transport.TransportError exceptions.
+
+        """
+        # Setup.
+        monkeypatch.delattr(locals(), "e", False)
+        fp = MockFP()
+        e_original = self.create_HTTPError(code=status_code, fp=fp)
+        t = suds.transport.http.HttpTransport()
+        t.urlopener = MockURLOpenerSaboteur(open_exception=e_original)
+        request = create_request()
+
+        # Execute.
+        e = pytest.raises(suds.transport.TransportError, t.open, request).value
+        try:
+            # Verify.
+            assert e.args == (str(e_original),)
+            assert e.httpcode is status_code
+            assert e.fp is fp
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    @pytest.mark.xfail(reason="original suds library bug")
+    @pytest.mark.parametrize("status_code", (
+        http_client.ACCEPTED,
+        http_client.NO_CONTENT))
+    def test_operation_invoke_with_urlopen_accept_no_content__data(self,
+            status_code):
+        """
+        suds.client.Client web service operation invocation expecting output
+        data, and for which a corresponding urlopen call raises a HTTPError
+        with status code ACCEPTED or NO_CONTENT, should report this as a
+        TransportError.
+
+        """
+        e = self.create_HTTPError(code=status_code)
+        transport = suds.transport.http.HttpTransport()
+        transport.urlopener = MockURLOpenerSaboteur(open_exception=e)
+        wsdl = testutils.wsdl('<xsd:element name="o" type="xsd:string"/>',
+            output="o", operation_name="f")
+        client = testutils.client_from_wsdl(wsdl, transport=transport)
+        pytest.raises(suds.transport.TransportError, client.service.f)
+
+    @pytest.mark.xfail(reason="original suds library bug")
+    @pytest.mark.parametrize("status_code", (
+        http_client.ACCEPTED,
+        http_client.NO_CONTENT))
+    def test_operation_invoke_with_urlopen_accept_no_content__no_data(self,
+            status_code):
+        """
+        suds.client.Client web service operation invocation expecting no output
+        data, and for which a corresponding urlopen call raises a HTTPError
+        with status code ACCEPTED or NO_CONTENT, should treat this as a
+        successful invocation.
+
+        """
+        # We are not yet sure that the behaviour checked for in this test is
+        # actually desired. The test is only an 'educated guess' prepared to
+        # demonstrate a related problem in the original suds library
+        # implementation. The original implementation is definitely buggy as
+        # its web service operation invocation raises an AttributeError
+        # exception by attempting to access a non-existing 'None.message'
+        # attribute internally.
+        e = self.create_HTTPError(code=status_code)
+        transport = suds.transport.http.HttpTransport()
+        transport.urlopener = MockURLOpenerSaboteur(open_exception=e)
+        wsdl = testutils.wsdl('<xsd:element name="o" type="xsd:string"/>',
+            output="o", operation_name="f")
+        client = testutils.client_from_wsdl(wsdl, transport=transport)
+        assert client.service.f() is None
+
+    def test_propagating_non_HTTPError_exceptions(self, send_method):
+        """
+        HttpTransport data sending operations need to propagate non-HTTPError
+        exceptions raised by the underlying urlopen call.
+
+        """
+        e = MyException()
+        t = suds.transport.http.HttpTransport()
+        t.urlopener = MockURLOpenerSaboteur(open_exception=e)
+        assert pytest.raises(e.__class__, t.open, create_request()).value is e
+
+    @pytest.mark.parametrize("status_code", (
+        http_client.RESET_CONTENT,
+        http_client.MOVED_PERMANENTLY,
+        http_client.BAD_REQUEST,
+        http_client.PAYMENT_REQUIRED,
+        http_client.FORBIDDEN,
+        http_client.NOT_FOUND,
+        http_client.INTERNAL_SERVER_ERROR,
+        http_client.NOT_IMPLEMENTED,
+        http_client.HTTP_VERSION_NOT_SUPPORTED))
+    def test_send_transforming_HTTPError_exceptions(self, status_code,
+            monkeypatch):
+        """
+        HttpTransport send() operation should transform HTTPError urlopener
+        exceptions with status codes other than ACCEPTED or NO_CONTENT to
+        suds.transport.TransportError exceptions.
+
+        """
+        # Setup.
+        monkeypatch.delattr(locals(), "e", False)
+        msg = object()
+        fp = MockFP()
+        e_original = self.create_HTTPError(msg=msg, code=status_code, fp=fp)
+        t = suds.transport.http.HttpTransport()
+        t.urlopener = MockURLOpenerSaboteur(open_exception=e_original)
+        request = create_request()
+
+        # Execute.
+        e = pytest.raises(suds.transport.TransportError, t.send, request).value
+        try:
+            # Verify.
+            assert len(e.args) == 1
+            assert e.args[0] is e_original.msg
+            assert e.httpcode is status_code
+            assert e.fp is fp
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+    @pytest.mark.parametrize("status_code", (
+        http_client.ACCEPTED,
+        http_client.NO_CONTENT))
+    def test_send_transforming_HTTPError_exceptions__accepted_no_content(self,
+            status_code):
+        """
+        HttpTransport send() operation should return None when their underlying
+        urlopen operation raises a HTTPError exception with status code
+        ACCEPTED or NO_CONTENT.
+
+        """
+        e_original = self.create_HTTPError(code=status_code)
+        t = suds.transport.http.HttpTransport()
+        t.urlopener = MockURLOpenerSaboteur(open_exception=e_original)
+        assert t.send(create_request()) is None
+
+    @pytest.mark.parametrize("url", test_URL_data)
+    def test_urlopener_default(self, url, send_method, monkeypatch):
+        """
+        HttpTransport builds a new urlopener if not given an external one.
+
+        """
+        def my_build_urlopener(*handlers):
+            assert len(handlers) == 1
+            assert handlers[0].__class__ is ProxyHandler
+            raise MyException
+        monkeypatch.setattr(urllib_request, "build_opener", my_build_urlopener)
+        transport = suds.transport.http.HttpTransport()
+        request = create_request(url=url)
+        pytest.raises(MyException, send_method, transport, request)
+
+    @pytest.mark.parametrize("url", test_URL_data)
+    def test_urlopener_indirection(self, url, send_method, monkeypatch):
+        """
+        HttpTransport may be configured with an external urlopener.
+
+        In that case, when opening or sending a HTTP request, a new urlopener
+        is not built and the given urlopener is used as-is, without adding any
+        extra handlers to it.
+
+        """
+        class MockURLOpener:
+            def open(self, request, timeout=None):
+                assert request.__class__ is urllib_request.Request
+                assert request.get_full_url() == url
+                raise MyException
+        transport = suds.transport.http.HttpTransport()
+        transport.urlopener = MockURLOpener()
+        def my_build_urlopener(*args, **kwargs):
+            pytest.fail("urllib build_opener() called when not expected.")
+        monkeypatch.setattr(urllib_request, "build_opener", my_build_urlopener)
+        request = create_request(url=url)
+        pytest.raises(MyException, send_method, transport, request)
 
 
 def _encode_basic_credentials(username, password):
     """
-      Encodes user credentials as used in basic HTTP authentication.
+    Encode user credentials as used in basic HTTP authentication.
 
-      This is the value expected to be added to the 'Authorization' HTTP
-    header.
+    This is the value expected to be added to the 'Authorization' HTTP header.
 
     """
     data = suds.byte_str("%s:%s" % (username, password))
     return "Basic %s" % base64.b64encode(data).decode("utf-8")
-
-
-def _wsdl_with_input_data(url):
-    """
-    Return a WSDL schema with a single operation f taking a single parameter.
-
-    Included operation takes a single string parameter and returns no values.
-    Externally specified URL is used as the web service location.
-
-    """
-    return suds.byte_str(u"""\
-<?xml version="1.0" encoding="utf-8"?>
-<wsdl:definitions targetNamespace="myNamespace"
-  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
-  xmlns:tns="myNamespace"
-  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
-  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <wsdl:types>
-    <xsd:schema targetNamespace="myNamespace">
-      <xsd:element name="fRequest" type="xsd:string"/>
-    </xsd:schema>
-  </wsdl:types>
-  <wsdl:message name="fInputMessage">
-    <wsdl:part name="parameters" element="tns:fRequest"/>
-  </wsdl:message>
-  <wsdl:portType name="Port">
-    <wsdl:operation name="f">
-      <wsdl:input message="tns:fInputMessage"/>
-    </wsdl:operation>
-  </wsdl:portType>
-  <wsdl:binding name="Binding" type="tns:Port">
-    <soap:binding style="document"
-      transport="http://schemas.xmlsoap.org/soap/http"/>
-    <wsdl:operation name="f">
-      <soap:operation/>
-      <wsdl:input><soap:body use="literal"/></wsdl:input>
-    </wsdl:operation>
-  </wsdl:binding>
-  <wsdl:service name="Service">
-    <wsdl:port name="Port" binding="tns:Binding">
-      <soap:address location="%s"/>
-    </wsdl:port>
-  </wsdl:service>
-</wsdl:definitions>""" % (url,))
-
-
-def _wsdl_with_no_input_data(url):
-    """
-    Return a WSDL schema with a single operation f taking no parameters.
-
-    Included operation returns no values. Externally specified URL is used as
-    the web service location.
-
-    """
-    return suds.byte_str(u"""\
-<?xml version="1.0" encoding="utf-8"?>
-<wsdl:definitions targetNamespace="myNamespace"
-  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
-  xmlns:tns="myNamespace"
-  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
-  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <wsdl:portType name="Port">
-    <wsdl:operation name="f"/>
-  </wsdl:portType>
-  <wsdl:binding name="Binding" type="tns:Port">
-    <soap:binding style="document"
-      transport="http://schemas.xmlsoap.org/soap/http"/>
-    <wsdl:operation name="f"/>
-  </wsdl:binding>
-  <wsdl:service name="Service">
-    <wsdl:port name="Port" binding="tns:Binding">
-      <soap:address location="%s"/>
-    </wsdl:port>
-  </wsdl:service>
-</wsdl:definitions>""" % (url,))
diff --git a/tests/test_xsd_builtins.py b/tests/test_xsd_builtins.py
new file mode 100644
index 0000000..c9a6b1f
--- /dev/null
+++ b/tests/test_xsd_builtins.py
@@ -0,0 +1,794 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds library's built-in XSD type handling unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+import testutils
+if __name__ == "__main__":
+    testutils.run_using_pytest(globals())
+
+import suds.client
+import suds.sax.date
+from suds.xsd.sxbuiltin import (Factory, XAny, XBoolean, XBuiltin, XDate,
+    XDateTime, XDecimal, XFloat, XInteger, XLong, XString, XTime)
+from testutils.compare_sax import CompareSAX
+
+import pytest
+
+import datetime
+import decimal
+import re
+import sys
+
+if sys.version_info >= (2, 6):
+    import fractions
+if sys.version_info >= (3,):
+    long = int
+
+
+class _Dummy:
+    """Class for testing unknown object class handling."""
+    pass
+
+
+# Define mock MockXType classes (e.g. MockXDate, MockXInteger & MockXString)
+# used to test translate() methods in different XSD data type model classes
+# such as XDate, XInteger & XString.
+def _def_mock_xsd_class(x_class_name):
+    """
+    Define a mock XType class and reference it globally as MockXType.
+
+    XType classes (e.g. XDate, XInteger & XString), represent built-in XSD
+    types. Their mock counterparts created here (e.g. MockXDate, MockXInteger &
+    MockXString) may be used to test their translate() methods without having
+    to connect them to an actual XSD schema.
+
+    This is achieved by having their constructor call take no parameters and
+    not call the parent class __init__() method.
+
+    Rationale why these mock classes are used instead of actual XType classes:
+      * XType instances need to be connected to an actual XSD schema element
+        which would unnecessarily complicate our test code.
+      * XType translate() implementations are not affected by whether the
+        instance they have been called on has been connected to an actual XSD
+        schema element.
+      * XType translate() functions can not be called as unbound methods, e.g.
+        XDate.translate(...). Such an implementation would fail if those
+        methods are not defined exactly in the specified XType class but in one
+        of its parent classes.
+
+    """
+    x_class = getattr(suds.xsd.sxbuiltin, x_class_name)
+    assert issubclass(x_class, XBuiltin)
+    mock_class_name = "Mock" + x_class_name
+    mock_class = type(mock_class_name, (x_class,), {
+        "__doc__": "Mock %s not connected to an XSD schema." % (x_class_name,),
+        "__init__": lambda self: None})
+    globals()[mock_class_name] = mock_class
+
+for x in ("XAny", "XBoolean", "XDate", "XDateTime", "XDecimal", "XFloat",
+        "XInteger", "XLong", "XString", "XTime"):
+    _def_mock_xsd_class(x)
+
+
+# Built-in XSD data types as defined in 'XML Schema Part 2: Datatypes Second
+# Edition' (http://www.w3.org/TR/2004/REC-xmlschema-2-20041028). Each is paired
+# with its respective suds library XSD type modeling class.
+builtins = {
+    "anySimpleType": XString,
+    "anyType": XAny,
+    "anyURI": XString,
+    "base64Binary": XString,
+    "boolean": XBoolean,
+    "byte": XInteger,
+    "date": XDate,
+    "dateTime": XDateTime,
+    "decimal": XDecimal,
+    "double": XFloat,
+    "duration": XString,
+    "ENTITIES": XString,
+    "ENTITY": XString,
+    "float": XFloat,
+    "gDay": XString,
+    "gMonth": XString,
+    "gMonthDay": XString,
+    "gYear": XString,
+    "gYearMonth": XString,
+    "hexBinary": XString,
+    "ID": XString,
+    "IDREF": XString,
+    "IDREFS": XString,
+    "int": XInteger,
+    "integer": XInteger,
+    "language": XString,
+    "long": XLong,
+    "Name": XString,
+    "NCName": XString,
+    "negativeInteger": XInteger,
+    "NMTOKEN": XString,
+    "NMTOKENS": XString,
+    "nonNegativeInteger": XInteger,
+    "nonPositiveInteger": XInteger,
+    "normalizedString": XString,
+    "NOTATION": XString,
+    "positiveInteger": XInteger,
+    "QName": XString,
+    "short": XInteger,
+    "string": XString,
+    "time": XTime,
+    "token": XString,
+    "unsignedByte": XInteger,
+    "unsignedInt": XInteger,
+    "unsignedLong": XLong,
+    "unsignedShort": XInteger}
+
+# XML namespaces where all the built-in type names live, as defined in 'XML
+# Schema Part 2: Datatypes Second Edition'
+# (http://www.w3.org/TR/2004/REC-xmlschema-2-20041028).
+builtin_namespaces = [
+    "http://www.w3.org/2001/XMLSchema",
+    "http://www.w3.org/2001/XMLSchema-datatypes"]
+
+
+class TestXBoolean:
+    """suds.xsd.sxbuiltin.XBoolean.translate() tests."""
+
+    @pytest.mark.parametrize(("source", "expected"), (
+        (0, "false"),
+        (1, "true"),
+        (False, "false"),
+        (True, "true")))
+    def test_from_python_object(self, source, expected):
+        translated = MockXBoolean().translate(source, topython=False)
+        assert translated.__class__ is str
+        assert translated == expected
+
+    @pytest.mark.parametrize("source", (
+        None,
+        pytest.mark.skipif(sys.version_info >= (3,),
+            reason="int == long since Python 3.0")(long(0)),
+        pytest.mark.skipif(sys.version_info >= (3,),
+            reason="int == long since Python 3.0")(long(1)),
+        "x",
+        "True",
+        "False",
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1)))
+    def test_from_python_object__invalid(self, source):
+        assert MockXBoolean().translate(source, topython=False) is source
+
+    @pytest.mark.parametrize("source", (-1, 2, 5, 100))
+    def test_from_python_object__invalid_integer(self, source):
+        #TODO: See if this special integer handling is really desired.
+        assert MockXBoolean().translate(source, topython=False) is None
+
+    @pytest.mark.parametrize(("source", "expected"), (
+        ("0", False),
+        ("1", True),
+        ("false", False),
+        ("true", True)))
+    def test_to_python_object(self, source, expected):
+        assert MockXBoolean().translate(source) is expected
+
+    @pytest.mark.parametrize("source",
+        (0, 1, "", "True", "False", "2", "Z", "-1", "00", "x", "poppycock"))
+    def test_to_python_object__invalid(self, source):
+        assert MockXBoolean().translate(source) is None
+
+
+class TestXDate:
+    """
+    suds.xsd.sxbuiltin.XDate.translate() tests.
+
+    Related Python object <--> string conversion details are tested in a
+    separate date/time related test module. These tests are only concerned with
+    basic translate() functionality.
+
+    """
+
+    def test_from_python_object__date(self):
+        date = datetime.date(2013, 7, 24)
+        translated = MockXDate().translate(date, topython=False)
+        assert translated.__class__ is suds.sax.date.Date
+        assert str(translated) == "2013-07-24"
+
+    def test_from_python_object__datetime(self):
+        dt = datetime.datetime(2013, 7, 24, 11, 59, 4)
+        translated = MockXDate().translate(dt, topython=False)
+        assert translated.__class__ is suds.sax.date.Date
+        assert str(translated) == "2013-07-24"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.time()))
+    def test_from_python_object__invalid(self, source):
+        assert MockXDate().translate(source, topython=False) is source
+
+    def test_to_python_object(self):
+        assert MockXDate().translate("1941-12-7") == datetime.date(1941, 12, 7)
+
+    def test_to_python_object__empty_string(self):
+        assert MockXDate().translate("") == None
+
+
+class TestXDateTime:
+    """
+    suds.xsd.sxbuiltin.XDateTime.translate() tests.
+
+    Related Python object <--> string conversion details are tested in a
+    separate date/time related test module. These tests are only concerned with
+    basic translate() functionality.
+
+    """
+
+    def test_from_python_object(self):
+        dt = datetime.datetime(2021, 12, 31, 11, 25)
+        translated = MockXDateTime().translate(dt, topython=False)
+        assert translated.__class__ is suds.sax.date.DateTime
+        assert str(translated) == "2021-12-31T11:25:00"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.time(22, 47, 9, 981),
+        datetime.date(2101, 1, 1)))
+    def test_from_python_object__invalid(self, source):
+        assert MockXDateTime().translate(source, topython=False) is source
+
+    def test_to_python_object(self):
+        dt = datetime.datetime(1941, 12, 7, 10, 30, 22, 454000)
+        assert MockXDateTime().translate("1941-12-7T10:30:22.454") == dt
+
+    def test_to_python_object__empty_string(self):
+        assert MockXDateTime().translate("") == None
+
+
+class TestXDecimal:
+    """suds.xsd.sxbuiltin.XDecimal.translate() tests."""
+
+    @pytest.mark.parametrize(("source", "expected"), (
+        # Zeros.
+        (decimal.Decimal("0"), "0"),
+        (decimal.Decimal("-0"), "-0"),
+        # Positive integral.
+        (decimal.Decimal("1"), "1"),
+        (decimal.Decimal("1000"), "1000"),
+        (decimal.Decimal("1E500"), "1" + "0" * 500),
+        # Negative integral.
+        (decimal.Decimal("-1"), "-1"),
+        (decimal.Decimal("-1000"), "-1000"),
+        (decimal.Decimal("-1E500"), "-1" + "0" * 500),
+        # Simple fractional.
+        (decimal.Decimal("0.1"), "0.1"),
+        (decimal.Decimal("-0.1"), "-0.1"),
+        (decimal.Decimal("0." + "0123456789" * 9), "0." + "0123456789" * 9),
+        (decimal.Decimal("-0." + "0123456789" * 9), "-0." + "0123456789" * 9),
+        # Zero with extra fractional digits.
+        (decimal.Decimal("0.0000"), "0"),
+        (decimal.Decimal("-0.0000"), "-0"),
+        # Only 0s as fractional digits.
+        (decimal.Decimal("5.000000000000000000"), "5"),
+        (decimal.Decimal("-5.000000000000000000"), "-5"),
+        # Trailing fractional 0 digits.
+        (decimal.Decimal("5.000000123000000000000"), "5.000000123"),
+        (decimal.Decimal("-5.000000123000000000000"), "-5.000000123"),
+        # Very small fractional part.
+        (decimal.Decimal("9E-100"), "0." + "0" * 99 + "9"),
+        (decimal.Decimal("-9E-100"), "-0." + "0" * 99 + "9")))
+    def test_decimal_to_xsd_value_representation(self, source, expected):
+        assert source.__class__ is decimal.Decimal
+        string = MockXDecimal()._decimal_to_xsd_format(source)
+        assert string.__class__ is str
+        assert string == expected
+
+    @pytest.mark.parametrize("source", (
+        decimal.Decimal(0),
+        decimal.Decimal("0.1") + decimal.Decimal("0.2"),
+        decimal.Decimal("5.781963")))
+    def test_from_python_object(self, source):
+        assert source.__class__ is decimal.Decimal, "bad test data"
+        translated = MockXDecimal().translate(source, topython=False)
+        expected = MockXDecimal()._decimal_to_xsd_format(source)
+        assert translated.__class__ is str
+        assert translated == expected
+
+    extra_test_data = ()
+    if sys.version_info >= (2, 6):
+        extra_test_data = (
+            # fraction.Fraction
+            fractions.Fraction(10, 4),
+            fractions.Fraction(1, 3))
+    @pytest.mark.parametrize("source", (
+        None,
+        # bool
+        True,
+        False,
+        # float
+        -50.2,
+        1.9623e-26,
+        0.1 + 0.2,
+        0.7,
+        1.0,
+        50.99999,
+        # int
+        0,
+        1,
+        -55566,
+        # str
+        "0.1",
+        "0.2",
+        "x",
+        # other
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1)) + extra_test_data)
+    def test_from_python_object__no_translation(self, source):
+        assert MockXDecimal().translate(source, topython=False) is source
+
+    @pytest.mark.parametrize("source", (
+        "-500.0",
+        "0",
+        "0.0",
+        "0.00000000000000000000001",
+        "000",
+        "1.78123875",
+        "-1.78123875",
+        "1",
+        "01",
+        "100"))
+    def test_to_python_object(self, source):
+        translated = MockXDecimal().translate(source)
+        assert translated.__class__ is decimal.Decimal
+        assert translated == decimal.Decimal(source)
+
+    @pytest.mark.parametrize("source",
+        ("", 0, 1, 1.5, True, False, 500, _Dummy(), object()))
+    def test_to_python_object__invalid_class_or_empty_string(self, source):
+        assert MockXDecimal().translate(source) is None
+
+    @pytest.mark.parametrize("src", (" ", "0,0", "0-0", "x", "poppycock"))
+    def test_to_python_object__invalid_string(self, src):
+        """
+        Suds raises raw Python exceptions when it fails to convert received
+        response element data to its mapped Python decimal.Decimal data type,
+        according to the used WSDL schema.
+
+        """
+        pytest.raises(decimal.InvalidOperation, MockXDecimal().translate, src)
+
+
+class TestXFloat:
+    """suds.xsd.sxbuiltin.XFloat.translate() tests."""
+
+    extra_test_data = ()
+    if sys.version_info >= (2, 6):
+        extra_test_data = (
+            # fraction.Fraction
+            fractions.Fraction(10, 4),
+            fractions.Fraction(1, 3))
+    @pytest.mark.parametrize("source", (
+        None,
+        # bool
+        True,
+        False,
+        # decimal.Decimal
+        decimal.Decimal(0),
+        decimal.Decimal("0.1") + decimal.Decimal("0.2"),
+        decimal.Decimal("5.781963"),
+        # float
+        -50.2,
+        0.1 + 0.2,
+        0.7,
+        1.0,
+        50.99999,
+        # int
+        0,
+        1,
+        -55566,
+        # str
+        "0.1",
+        "0.2",
+        "x",
+        # other
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1)) + extra_test_data)
+    def test_from_python_object(self, source):
+        assert MockXFloat().translate(source, topython=False) is source
+
+    @pytest.mark.parametrize("source", (
+        "-500.0",
+        "0",
+        "0.0",
+        "0.00000000000000000000001",
+        "000",
+        "1.78123875",
+        "-1.78123875",
+        "1",
+        "01",
+        "100"))
+    def test_to_python_object(self, source):
+        translated = MockXFloat().translate(source)
+        assert translated.__class__ is float
+        assert translated == float(source)
+
+    @pytest.mark.parametrize("source",
+        ("", 0, 1, 1.5, True, False, 500, _Dummy(), object()))
+    def test_to_python_object__invalid_class_or_empty_string(self, source):
+        assert MockXFloat().translate(source) is None
+
+    @pytest.mark.parametrize("source", (" ", "0,0", "0-0", "x", "poppycock"))
+    def test_to_python_object__invalid_string(self, source, monkeypatch):
+        """
+        Suds raises raw Python exceptions when it fails to convert received
+        response element data to its mapped Python float data type, according
+        to the used WSDL schema.
+
+        """
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(ValueError, MockXFloat().translate, source).value
+        try:
+            # Using different Python interpreter versions and different source
+            # strings results in different exception messages here.
+            try:
+                float(source)
+                pytest.fail("Bad test data.")
+            except ValueError:
+                assert str(e) == str(sys.exc_info()[1])
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestXInteger:
+    """suds.xsd.sxbuiltin.XInteger.translate() tests."""
+
+    @pytest.mark.parametrize("source", (
+        None,
+        # bool
+        False,
+        True,
+        # int
+        -50,
+        0,
+        1,
+        50,
+        # long
+        long(-50),
+        long(0),
+        long(1),
+        long(50),
+        # str
+        "x",
+        # other
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1)))
+    def test_from_python_object(self, source):
+        assert MockXInteger().translate(source, topython=False) is source
+
+    @pytest.mark.parametrize("source", ("-500", "0", "000", "1", "01", "100"))
+    def test_to_python_object(self, source):
+        translated = MockXInteger().translate(source)
+        assert translated.__class__ is int
+        assert translated == int(source)
+
+    @pytest.mark.parametrize("source",
+        ("", 0, 1, True, False, 500, _Dummy(), object()))
+    def test_to_python_object__invalid_class_or_empty_string(self, source):
+        assert MockXInteger().translate(source) is None
+
+    @pytest.mark.parametrize("source", (" ", "0-0", "x", "poppycock"))
+    def test_to_python_object__invalid_string(self, source, monkeypatch):
+        """
+        Suds raises raw Python exceptions when it fails to convert received
+        response element data to its mapped Python integer data type, according
+        to the used WSDL schema.
+
+        """
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(ValueError, MockXInteger().translate, source).value
+        try:
+            # Using different Python interpreter versions and different source
+            # strings results in different exception messages here.
+            try:
+                int(source)
+                pytest.fail("Bad test data.")
+            except ValueError:
+                assert str(e) == str(sys.exc_info()[1])
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestXLong:
+    """suds.xsd.sxbuiltin.XLong.translate() tests."""
+
+    @pytest.mark.parametrize("source", (
+        None,
+        # bool
+        False,
+        True,
+        # int
+        -50,
+        0,
+        1,
+        50,
+        # long
+        long(-50),
+        long(0),
+        long(1),
+        long(50),
+        # str
+        "x",
+        # other
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1)))
+    def test_from_python_object(self, source):
+        assert MockXLong().translate(source, topython=False) is source
+
+    @pytest.mark.parametrize(("source", "expected"), (
+        ("-500", -500),
+        ("0", 0),
+        ("000", 0),
+        ("1", 1),
+        ("01", 1),
+        ("100", 100)))
+    def test_to_python_object(self, source, expected):
+        translated = MockXLong().translate(source)
+        assert translated.__class__ is long
+        assert translated == expected
+
+    @pytest.mark.parametrize("source",
+        ("", 0, 1, True, False, 500, _Dummy(), object()))
+    def test_to_python_object__invalid_class_or_empty_string(self, source):
+        assert MockXLong().translate(source) is None
+
+    @pytest.mark.parametrize("source", (" ", "0-0", "x", "poppycock"))
+    def test_to_python_object__invalid_string(self, source, monkeypatch):
+        """
+        Suds raises raw Python exceptions when it fails to convert received
+        response element data to its mapped Python long data type, according to
+        the used WSDL schema.
+
+        """
+        monkeypatch.delitem(locals(), "e", False)
+        e = pytest.raises(ValueError, MockXLong().translate, source).value
+        try:
+            # Using different Python interpreter versions and different source
+            # strings results in different exception messages here.
+            try:
+                long(source)
+                pytest.fail("Bad test data.")
+            except ValueError:
+                assert str(e) == str(sys.exc_info()[1])
+        finally:
+            del e  # explicitly break circular reference chain in Python 3
+
+
+class TestXTime:
+    """
+    suds.xsd.sxbuiltin.XTime.translate() tests.
+
+    Related Python object <--> string conversion details are tested in a
+    separate date/time related test module. These tests are only concerned with
+    basic translate() functionality.
+
+    """
+
+    def test_from_python_object(self):
+        time = datetime.time(16, 53, 12)
+        translated = MockXTime().translate(time, topython=False)
+        assert translated.__class__ is suds.sax.date.Time
+        assert str(translated) == "16:53:12"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1),
+        datetime.datetime(2101, 1, 1, 22, 47, 9, 981)))
+    def test_from_python_object__invalid(self, source):
+        assert MockXTime().translate(source, topython=False) is source
+
+    def test_to_python_object(self):
+        assert MockXTime().translate("10:30:22") == datetime.time(10, 30, 22)
+
+    def test_to_python_object__empty_string(self):
+        assert MockXTime().translate("") == None
+
+
+ at pytest.mark.parametrize(("xsd_type_name", "xsd_type"),
+    sorted(builtins.items()) +  # See 'Project implementation note #1'.
+    [("...unknown...", XBuiltin)])
+def test_create_builtin_type_schema_objects(xsd_type_name, xsd_type):
+    schema = _create_dummy_schema()
+    xsd_object = Factory.create(schema, xsd_type_name)
+    assert xsd_object.__class__ is xsd_type
+    assert xsd_object.name == xsd_type_name
+    assert xsd_object.schema is schema
+
+
+ at pytest.mark.parametrize("xsd_type_name", ("tonkica-polonkica", "integer"))
+def test_create_custom_mapped_builtin_type_schema_objects(xsd_type_name,
+        monkeypatch):
+    """User code can add or update built-in XSD type registrations."""
+    _monkeypatch_builtin_XSD_type_registry(monkeypatch)
+    class MockType:
+        def __init__(self, schema, name):
+            self.schema = schema
+            self.name = name
+    schema = _Dummy()
+    Factory.maptag(xsd_type_name, MockType)
+    xsd_object = Factory.create(schema, xsd_type_name)
+    assert xsd_object.__class__ is MockType
+    assert xsd_object.name == xsd_type_name
+    assert xsd_object.schema is schema
+
+
+# See 'Project implementation note #1'.
+ at pytest.mark.parametrize("name", sorted(builtins.keys()))
+ at pytest.mark.parametrize("namespace", builtin_namespaces)
+def test_recognize_builtin_types(name, namespace):
+    schema = _create_dummy_schema()
+    assert schema.builtin((name, namespace))
+
+
+# See 'Project implementation note #1'.
+ at pytest.mark.parametrize("name", sorted(builtins.keys()))
+ at pytest.mark.parametrize("namespace", ("", " ", "some-dummy-namespace"))
+def test_recognize_builtin_types_in_unknown_namespace(name, namespace):
+    schema = _create_dummy_schema()
+    assert not schema.builtin((name, namespace))
+
+
+ at pytest.mark.parametrize("name", ("", " ", "x", "xyz"))
+ at pytest.mark.parametrize("namespace", builtin_namespaces +
+    ["", " ", "some-dummy-namespace"])
+def test_recognize_non_builtin_types(name, namespace):
+    schema = _create_dummy_schema()
+    assert not schema.builtin((name, namespace))
+
+
+def test_recognize_custom_mapped_builtins(monkeypatch):
+    """User code can register additional XSD built-ins."""
+    _monkeypatch_builtin_XSD_type_registry(monkeypatch)
+    schema = _create_dummy_schema()
+    name = "trla-baba-lan"
+    for ns in builtin_namespaces:
+        assert not schema.builtin((name, ns))
+    Factory.maptag(name, _Dummy)
+    for ns in builtin_namespaces:
+        assert schema.builtin((name, ns))
+
+
+def test_resolving_builtin_types(monkeypatch):
+    _monkeypatch_builtin_XSD_type_registry(monkeypatch)
+    class MockXInteger(XInteger):
+        pass
+    Factory.maptag("osama", MockXInteger)
+
+    wsdl = testutils.wsdl('<xsd:element name="wu" type="xsd:osama"/>',
+        input="wu")
+    client = testutils.client_from_wsdl(wsdl)
+
+    element, schema_object = client.sd[0].params[0]
+    assert element.name == "wu"
+    assert element.type == ("osama", "http://www.w3.org/2001/XMLSchema")
+    assert schema_object.__class__ is MockXInteger
+    assert schema_object.name == "osama"
+    assert schema_object.schema is client.wsdl.schema
+
+
+def test_translation(monkeypatch):
+    """Python <--> XML representation translation on marshall/unmarshall."""
+    anObject = _Dummy()
+    class MockType(XBuiltin):
+        def __init__(self, *args, **kwargs):
+            self._mock_translate_log = []
+            super(MockType, self).__init__(*args, **kwargs)
+        def translate(self, value, topython=True):
+            self._mock_translate_log.append((value, topython))
+            if topython:
+                return anObject
+            return "'ollywood"
+    _monkeypatch_builtin_XSD_type_registry(monkeypatch)
+    Factory.maptag("woof", MockType)
+
+    namespace = "I'm a little tea pot, short and stout..."
+    wsdl = testutils.wsdl("""\
+      <xsd:element name="wi" type="xsd:woof"/>
+      <xsd:element name="wo" type="xsd:woof"/>""", input="wi", output="wo",
+        xsd_target_namespace=namespace, operation_name="f")
+    client = testutils.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+
+    # Check suds library's XSD schema input parameter information.
+    schema = client.wsdl.schema
+    element_in = schema.elements["wi", namespace]
+    assert element_in.name == "wi"
+    element_out = schema.elements["wo", namespace]
+    assert element_out.name == "wo"
+    schema_object_in = element_in.resolve()
+    schema_object_out = element_out.resolve()
+    assert element_in is client.sd[0].params[0][0]
+    assert schema_object_in is client.sd[0].params[0][1]
+    assert schema_object_in.__class__ is MockType
+    assert schema_object_in._mock_translate_log == []
+    assert schema_object_out.__class__ is MockType
+    assert schema_object_out._mock_translate_log == []
+
+    # Construct operation invocation request - test marshalling.
+    request = client.service.f(55)
+    assert schema_object_in._mock_translate_log == [(55, False)]
+    assert schema_object_out._mock_translate_log == []
+    CompareSAX.data2data(request.envelope, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+   <Header/>
+   <Body>
+      <wi xmlns="%s">'ollywood</wi>
+   </Body>
+</Envelope>""" % (namespace,))
+
+    # Process operation response - test unmarshalling.
+    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+  <Body>
+    <wo xmlns="%s">fri-fru</wo>
+  </Body>
+</Envelope>""" % (namespace,))))
+    assert response is anObject
+    assert schema_object_in._mock_translate_log == [(55, False)]
+    assert schema_object_out._mock_translate_log == [("fri-fru", True)]
+
+
+def _create_dummy_schema():
+    """Constructs a new dummy XSD schema instance."""
+    #TODO: Find out how to construct this XSD schema object directly without
+    # first having to construct a suds.client.Client from a complete WSDL
+    # schema.
+    wsdl = testutils.wsdl('<xsd:element name="dummy"/>', input="dummy")
+    client = testutils.client_from_wsdl(wsdl)
+    return client.wsdl.schema
+
+
+def _monkeypatch_builtin_XSD_type_registry(monkeypatch):
+    """
+    Monkeypatches the global suds built-in XSD type dictionary.
+
+    After calling this function, a test is free to mess around with suds
+    library's built-in XSD type register (register new ones, change classes
+    registered for a particular XSD type, remove registrations, and such) and
+    any such changes will be automatically undone at the end of the test.
+
+    If a test does not call this function, any such modifications will be left
+    valid in the current global application state and may affect tests run
+    afterwards.
+
+    """
+    tags = Factory.tags
+    assert tags.__class__ is dict
+    monkeypatch.setattr(Factory, "tags", dict(tags))
diff --git a/tests/test_xsd_element.py b/tests/test_xsd_element.py
new file mode 100644
index 0000000..58d32f3
--- /dev/null
+++ b/tests/test_xsd_element.py
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds library's XSD Element note unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import testutils
+    testutils.run_using_pytest(globals())
+
+
+import suds
+import suds.options
+import suds.sax.parser
+import suds.store
+import suds.xsd.schema
+
+import pytest
+from six import b
+
+
+# shared test input data
+form_test_values = (None, "qualified", "unqualified", "invalid", "")
+
+
+class TestElementForm:
+    """Test whether specific XSD elements are considered qualified."""
+
+    @pytest.mark.parametrize("form_default, form", [(x, y)
+        for x in form_test_values
+        for y in form_test_values])
+    def test_local_element(self, form_default, form):
+        parent_name = "Parent"
+        element_name = "Elemento"
+        namespace = "tns"
+        schema_xml = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        targetNamespace="%(namespace)s"%(form_default)s>
+    <element name="%(parent_name)s">
+        <complexType>
+            <sequence>
+                <element name="%(element_name)s"%(form)s/>
+            </sequence>
+        </complexType>
+    </element>
+</schema>""" % {
+            "element_name": element_name,
+            "form": _attribute_xml("form", form),
+            "form_default": _attribute_xml("elementFormDefault", form_default),
+            "namespace": namespace,
+            "parent_name": parent_name}
+        expected = form == "qualified" or (
+            (form is None) and (form_default == "qualified"))
+        schema = _parse_schema_xml(b(schema_xml))
+        parent_element = schema.elements[parent_name, namespace]
+        element = parent_element.get_child(element_name)[0]
+        assert bool(element.form_qualified) == expected
+
+    @pytest.mark.parametrize("form_default, form, form_referenced", [(x, y, z)
+        for x in form_test_values
+        for y in form_test_values
+        for z in form_test_values])
+    def test_reference_to_internal(self, form_default, form, form_referenced):
+        """Reference element to an element in the same schema."""
+        referenced_name = "Referenced"
+        referencing_parent_name = "Referencing"
+        namespace = "tns"
+        schema_xml = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        xmlns:tns="%(namespace)s"
+        targetNamespace="%(namespace)s"%(form_default)s>
+    <element name="%(referenced_name)s"%(form_referenced)s/>
+    <element name="%(referencing_parent_name)s">
+        <complexType>
+            <sequence>
+                <element ref="tns:%(referenced_name)s"%(form_referencing)s/>
+            </sequence>
+        </complexType>
+    </element>
+</schema>""" % {
+            "form_default": _attribute_xml("elementFormDefault", form_default),
+            "form_referenced": _attribute_xml("form", form_referenced),
+            "form_referencing": _attribute_xml("form", form),
+            "namespace": namespace,
+            "referenced_name": referenced_name,
+            "referencing_parent_name": referencing_parent_name}
+        schema = _parse_schema_xml(b(schema_xml))
+        parent_element = schema.elements[referencing_parent_name, namespace]
+        referencing_element = parent_element.get_child(referenced_name)[0]
+        assert referencing_element.form_qualified
+
+    def test_reference_to_external(self):
+        """Reference element to an element in an external schema."""
+        schema_xml_here = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:there="ns-there">
+    <import namespace="ns-there" schemaLocation="suds://there.xsd"/>
+    <element name="Referencing">
+        <complexType>
+            <sequence>
+                <element ref="there:Referenced"/>
+            </sequence>
+        </complexType>
+    </element>
+</schema>"""
+        schema_xml_there = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="ns-there">
+    <element name="Referenced"/>
+</schema>"""
+        store = suds.store.DocumentStore({"there.xsd": b(schema_xml_there)})
+        schema = _parse_schema_xml(b(schema_xml_here), store)
+        referenced_element = schema.elements["Referenced", "ns-there"]
+        referencing_parent = schema.elements["Referencing", None]
+        referencing_element = referencing_parent.get_child("Referenced")[0]
+        assert referencing_element.form_qualified
+
+    @pytest.mark.parametrize("form_default, form", [(x, y)
+        for x in form_test_values
+        for y in form_test_values])
+    def test_top_level_element(self, form_default, form):
+        element_name = "Elemento"
+        namespace = "tns"
+        schema_xml = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        targetNamespace="%(namespace)s"%(form_default)s>
+    <element name="%(element_name)s"%(form)s/>
+</schema>""" % {
+            "element_name": element_name,
+            "form": _attribute_xml("form", form),
+            "form_default": _attribute_xml("elementFormDefault", form_default),
+            "namespace": namespace}
+        schema = _parse_schema_xml(b(schema_xml))
+        element = schema.elements[element_name, namespace]
+        assert element.form_qualified
+
+
+def test_reference():
+    """Reference to an element in a different schema."""
+    schema_xml_here = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:there="ns-there">
+    <import namespace="ns-there" schemaLocation="suds://there.xsd"/>
+    <element name="Referencing">
+        <complexType>
+            <sequence>
+                <element ref="there:Referenced"/>
+            </sequence>
+        </complexType>
+    </element>
+</schema>"""
+    schema_xml_there = """\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="ns-there">
+    <element name="Referenced"/>
+</schema>"""
+    store = suds.store.DocumentStore({"there.xsd": b(schema_xml_there)})
+    schema = _parse_schema_xml(b(schema_xml_here), store)
+    referenced_element = schema.elements["Referenced", "ns-there"]
+    referencing_parent = schema.elements["Referencing", None]
+    referencing_element = referencing_parent.get_child("Referenced")[0]
+    assert referenced_element.ref is None
+    assert referencing_element.ref == ("Referenced", "ns-there")
+
+
+###############################################################################
+#
+# Test utilities.
+#
+###############################################################################
+
+def _attribute_xml(name, value):
+    if value is not None:
+        return ' %s="%s"' % (name, value)
+    return ""
+
+
+def _parse_schema_xml(xml, documentStore=None):
+    """Test utility constructing an XSD schema model from the given XML."""
+    parser = suds.sax.parser.Parser()
+    document = parser.parse(string=xml)
+    root = document.root()
+    url = "somewhere://over.the/rainbow"
+    options_kwargs = {}
+    if documentStore:
+        options_kwargs.update(documentStore=documentStore)
+    options = suds.options.Options(**options_kwargs)
+    return suds.xsd.schema.Schema(root, url, options)
diff --git a/tests/testutils/__init__.py b/tests/testutils/__init__.py
new file mode 100644
index 0000000..c716ab1
--- /dev/null
+++ b/tests/testutils/__init__.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Package containing different utilities used for suds project testing.
+
+"""
+
+import suds.client
+import suds.store
+
+import os
+import subprocess
+import sys
+
+
+def client_from_wsdl(wsdl_content, *args, **kwargs):
+    """
+    Constructs a non-caching suds Client based on the given WSDL content.
+
+      The wsdl_content is expected to be a raw byte string and not a unicode
+    string. This simple structure suits us fine here because XML content holds
+    its own embedded encoding identification ('utf-8' if not specified
+    explicitly).
+
+      Stores the content directly inside the suds library internal document
+    store under a hard-coded id to avoid having to load the data from a
+    temporary file.
+
+      Uses a locally created empty document store unless one is provided
+    externally using the 'documentStore' keyword argument.
+
+      Explicitly disables caching or otherwise, because we use the same
+    hardcoded id for our main WSDL document, suds would always reuse the first
+    such local document from its cache instead of fetching it from our document
+    store.
+
+    """
+    assert wsdl_content.__class__ is suds.byte_str_class, "bad test data"
+    store = kwargs.get("documentStore")
+    if store is None:
+        store = suds.store.DocumentStore()
+        kwargs.update(documentStore=store)
+    test_file_id = "whatchamacallit"
+    store.update({test_file_id: wsdl_content})
+    kwargs.update(cache=None)
+    return suds.client.Client("suds://" + test_file_id, *args, **kwargs)
+
+
+def run_test_process(script):
+    """
+    Runs the given Python test script as a separate process.
+
+    Expects the script to return an exit code 0 and output nothing on either
+    stdout or stderr output streams.
+
+    """
+    popen = subprocess.Popen([sys.executable], stdin=subprocess.PIPE,
+        stderr=subprocess.PIPE, stdout=subprocess.PIPE, cwd=script.dirname,
+        universal_newlines=True)
+    sys_path = sys.path
+    for i in range(len(sys_path)):
+        if not sys_path[i]:
+            sys_path[i] = os.getcwd()
+    out, err = popen.communicate("""\
+import sys
+sys.path = %(sys.path)s
+import suds
+if suds.__version__ != %(suds.__version__)r:
+    print("Unexpected suds version imported - '%%s'." %% (suds.__version__))
+    sys.exit(-2)
+
+if sys.version_info >= (3, 0):
+    def exec_file(x):
+        e = getattr(__builtins__, "exec")
+        return e(open(x).read(), globals(), globals())
+else:
+    exec_file = execfile
+exec_file(%(script)r)
+""" % {"suds.__version__": suds.__version__,
+    "script": script.basename,
+    "sys.path": sys_path})
+    if popen.returncode != 0 or err or out:
+        if popen.returncode != 0:
+            print("Test process exit code: %d" % (popen.returncode,))
+        if out:
+            print("Test process stdout:")
+            print(out)
+        if err:
+            print("Test process stderr:")
+            print(err)
+        import pytest
+        pytest.fail("Test subprocess failed.")
+
+
+def run_using_pytest(caller_globals):
+    """Run the caller test script using the pytest testing framework."""
+    import sys
+    # Trick setuptools into not recognizing we are referencing __file__ here.
+    # If setuptools detects __file__ usage in a module, any package containing
+    # this module will be installed as an actual folder instead of a zipped
+    # archive. This __file__ usage is safe since it is used only when a script
+    # has been run directly, and that can not be done from a zipped package
+    # archive.
+    filename = caller_globals.get("file".join(["__"] * 2))
+    if not filename:
+        sys.exit("Internal error: can not determine test script name.")
+    try:
+        import pytest
+    except ImportError:
+        filename = filename or "<unknown-script>"
+        sys.exit("'py.test' unit testing framework not available. Can not run "
+            "'%s' directly as a script." % (filename,))
+    exit_code = pytest.main(["--pyargs", filename] + sys.argv[1:])
+    sys.exit(exit_code)
+
+
+def wsdl(schema_content, input=None, output=None, operation_name="f",
+        wsdl_target_namespace="my-wsdl-namespace",
+        xsd_target_namespace="my-xsd-namespace",
+        web_service_URL="protocol://unga-bunga-location"):
+    """
+    Returns WSDL schema content used in different suds library tests.
+
+    Defines a single operation taking an externally specified input structure
+    and returning an externally defined output structure.
+
+    Constructed WSDL schema's XML namespace prefixes:
+      * my_wsdl - the WSDL schema's target namespace.
+      * my_xsd - the embedded XSD schema's target namespace.
+
+    input/output parameters accept the following values:
+      * None - operation has no input/output message.
+      * list/tuple - operation has an input/output message consisting of
+        message parts referencing top-level XSD schema elements with the given
+        names.
+      * Otherwise operation has an input/output message consisting of a single
+        message part referencing a top-level XSD schema element with the given
+        name.
+
+    """
+    has_input = input is not None
+    has_output = output is not None
+
+    wsdl = ["""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="%(wsdl_target_namespace)s"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:my_wsdl="%(wsdl_target_namespace)s"
+xmlns:my_xsd="%(xsd_target_namespace)s"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="%(xsd_target_namespace)s"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+%(schema_content)s
+    </xsd:schema>
+  </wsdl:types>""" % dict(schema_content=schema_content,
+        wsdl_target_namespace=wsdl_target_namespace,
+        xsd_target_namespace=xsd_target_namespace)]
+
+    if has_input:
+        if input.__class__ not in (list, tuple):
+            input = [input]
+        wsdl.append("""\
+  <wsdl:message name="fRequestMessage">""")
+        for element in input:
+            wsdl.append("""\
+    <wsdl:part name="parameters" element="my_xsd:%s" />""" % (element,))
+        wsdl.append("""\
+  </wsdl:message>""")
+
+    if has_output:
+        if output.__class__ not in (list, tuple):
+            output = [output]
+        wsdl.append("""\
+  <wsdl:message name="fResponseMessage">""")
+        for element in output:
+            wsdl.append("""\
+    <wsdl:part name="parameters" element="my_xsd:%s" />""" % (element,))
+        wsdl.append("""\
+  </wsdl:message>""")
+
+    wsdl.append("""\
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="%s">""" % (operation_name,))
+
+    if has_input:
+        wsdl.append("""\
+      <wsdl:input message="my_wsdl:fRequestMessage" />""")
+    if has_output:
+        wsdl.append("""\
+      <wsdl:output message="my_wsdl:fResponseMessage" />""")
+
+    wsdl.append("""\
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="my_wsdl:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="%s">
+      <soap:operation soapAction="my-soap-action" style="document" />""" %
+        (operation_name,))
+
+    if has_input:
+        wsdl.append("""\
+      <wsdl:input><soap:body use="literal" /></wsdl:input>""")
+    if has_output:
+        wsdl.append("""\
+      <wsdl:output><soap:body use="literal" /></wsdl:output>""")
+
+    wsdl.append("""\
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="my_wsdl:dummy">
+      <soap:address location="%s" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""" % (web_service_URL,))
+
+    return suds.byte_str("\n".join(wsdl))
diff --git a/tests/conftest.py b/tests/testutils/assertion.py
similarity index 55%
copy from tests/conftest.py
copy to tests/testutils/assertion.py
index c706614..f514552 100644
--- a/tests/conftest.py
+++ b/tests/testutils/assertion.py
@@ -1,31 +1,23 @@
-# -*- coding: utf-8 -*-
-
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
-
-"""
-pytest configuration file for the suds test suite.
-
-"""
-
-# Make pytest load custom plugins expected to be loaded in our test suite.
-#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
-# information in its --markers list if called from a folder other than the one
-# containing the tests folder or if the tests folder is not on the current
-# Python path, e.g. if using pytest in the Python 3 implementation 'build'
-# folder constructed by 'setup.py build' using 'py.test build --markers'. The
-# plugin will still get loaded correctly when actually running the tests. This
-# has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Assertion test utility functions shared between multiple test modules.
+
+"""
+
+from testutils.assertion__pytest_assert_rewrite_needed import *
diff --git a/tests/conftest.py b/tests/testutils/assertion__pytest_assert_rewrite_needed.py
similarity index 55%
copy from tests/conftest.py
copy to tests/testutils/assertion__pytest_assert_rewrite_needed.py
index c706614..2d0af3b 100644
--- a/tests/conftest.py
+++ b/tests/testutils/assertion__pytest_assert_rewrite_needed.py
@@ -1,31 +1,41 @@
-# -*- coding: utf-8 -*-
-
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
-
-"""
-pytest configuration file for the suds test suite.
-
-"""
-
-# Make pytest load custom plugins expected to be loaded in our test suite.
-#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
-# information in its --markers list if called from a folder other than the one
-# containing the tests folder or if the tests folder is not on the current
-# Python path, e.g. if using pytest in the Python 3 implementation 'build'
-# folder constructed by 'setup.py build' using 'py.test build --markers'. The
-# plugin will still get loaded correctly when actually running the tests. This
-# has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Assertion test utility functions shared between multiple test modules.
+
+Extracted into a separate module, named so pytest would apply its assertion
+rewriting to it.
+
+'pytest' assertion rewriting allows our assertion test utility functions to use
+Python assertions and have them work even when run with Python assertions
+disabled.
+
+"""
+
+
+def assert_no_output(pytest_capture_fixture):
+    """
+    Test utility asserting there was no captured stdout or stderr output.
+
+    pytest_capture_fixture parameter may be one of pytest's output capture
+    fixtures, e.g. capsys or capfd.
+
+    """
+    out, err = pytest_capture_fixture.readouterr()
+    assert not out
+    assert not err
diff --git a/tests/conftest.py b/tests/testutils/compare_sax.py
similarity index 55%
copy from tests/conftest.py
copy to tests/testutils/compare_sax.py
index c706614..473fbed 100644
--- a/tests/conftest.py
+++ b/tests/testutils/compare_sax.py
@@ -1,31 +1,23 @@
-# -*- coding: utf-8 -*-
-
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
-
-"""
-pytest configuration file for the suds test suite.
-
-"""
-
-# Make pytest load custom plugins expected to be loaded in our test suite.
-#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
-# information in its --markers list if called from a folder other than the one
-# containing the tests folder or if the tests folder is not on the current
-# Python path, e.g. if using pytest in the Python 3 implementation 'build'
-# folder constructed by 'setup.py build' using 'py.test build --markers'. The
-# plugin will still get loaded correctly when actually running the tests. This
-# has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+CompareSAX test utility class.
+
+"""
+
+from testutils.compare_sax__pytest_assert_rewrite_needed import *
diff --git a/tests/testutils/compare_sax__pytest_assert_rewrite_needed.py b/tests/testutils/compare_sax__pytest_assert_rewrite_needed.py
new file mode 100644
index 0000000..0aa133c
--- /dev/null
+++ b/tests/testutils/compare_sax__pytest_assert_rewrite_needed.py
@@ -0,0 +1,298 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+CompareSAX test utility class implementation.
+
+Extracted into a separate module, named so pytest would apply its assertion
+rewriting to it.
+
+'pytest' assertion rewriting allows CompareSAX to use Python assertion based
+XML mismatch reporting and have it work even when run with Python assertions
+disabled.
+
+"""
+
+import suds.sax.document
+import suds.sax.element
+import suds.sax.parser
+
+from six import text_type, u
+
+import sys
+
+
+class CompareSAX:
+    """
+    Support for comparing SAX XML structures.
+
+    Not intended to be perfect, but only good enough XML comparison to be used
+    internally inside the project's test suite.
+
+    Raw XML data is parsed using a suds SAX parser and the resulting DOM
+    structure compared. This means that any XML data differences lost during
+    SAX parsing can not be detected. Some examples:
+      - all textual content for a single node is concatenated together, so the
+        following two XML data segments are considered equivalent:
+          1. '<a>xx<b/>yy</a>'
+          2. '<a>xxyy<b/></a>'
+      - all leading and trailing whitespace is trimmed from textual content for
+        nodes having at least one child element, so most XML indentation
+        information is lost and the following two XML data segments are
+        considered equivalent:
+          1. '<a>   <b/>     </a>'
+          2. '<a><b/></a>'
+
+    Suds may generate different SOAP request XML data for the same input based
+    on the order in which it reads some of its internal data held inside
+    unordered containers, e.g. set or dictionary. To compensate for this we
+    consider both namespace prefix & namespace declaration (either extra
+    declarations or declaration placement) differences in XML documents as
+    irrelevant. We do however compare each XML node's namespace, i.e. that
+    their namespace names match even if their namespace prefixes do not or if
+    those namespaces have been declared on different XML elements.
+
+    """
+
+    def __init__(self):
+        self.__context = []
+
+    @staticmethod
+    def assertions_enabled():
+        """
+        Returns whether Python assertions have been enabled in this module.
+
+        CompareSAX class uses Python assertions to report failed comparison
+        results so this information in order to know whether related tests
+        should be disabled.
+
+        """
+        try:
+            assert False
+        except AssertionError:
+            return True
+        return False
+
+    @classmethod
+    def document2document(cls, lhs, rhs):
+        """Compares two SAX XML documents."""
+        self = cls()
+        try:
+            self.__document2document(lhs, rhs, context="document2document")
+        except Exception:
+            self.__report_context()
+            raise
+
+    @classmethod
+    def document2element(cls, document, element):
+        """
+        Compares a SAX XML document structure to a SAX XML element.
+
+        The given document & element are considered successfully matched if the
+        document consists of a single XML element matching the given one.
+
+        """
+        self = cls()
+        self.__push_context("document2element")
+        try:
+            assert document.__class__ is suds.sax.document.Document
+            assert element.__class__ is suds.sax.element.Element
+            assert len(document.getChildren()) == 1
+            self.__element2element(document.getChildren()[0], element)
+        except Exception:
+            self.__report_context()
+            raise
+
+    @classmethod
+    def element2element(cls, lhs, rhs):
+        """Compares two SAX XML elements."""
+        self = cls()
+        self.__push_context("element2element")
+        try:
+            self.__element2element(lhs, rhs)
+        except Exception:
+            self.__report_context()
+            raise
+
+    @classmethod
+    def data2data(cls, lhs, rhs):
+        """Compares two SAX XML documents given as strings or bytes objects."""
+        self = cls()
+        try:
+            lhs_doc = self.__parse_data(lhs)
+            rhs_doc = self.__parse_data(rhs)
+            self.__document2document(lhs_doc, rhs_doc, context="data2data")
+        except Exception:
+            self.__report_context()
+            raise
+
+    @classmethod
+    def document2data(cls, lhs, rhs):
+        """
+        Compares two SAX XML documents, second one given as a string or a bytes
+        object.
+
+        """
+        self = cls()
+        try:
+            rhs_doc = self.__parse_data(rhs)
+            self.__document2document(lhs, rhs_doc, context="document2data")
+        except Exception:
+            self.__report_context()
+            raise
+
+    def __compare_child_elements(self, lhs, rhs):
+        """Compares the given entities' child elements."""
+        assert len(lhs.getChildren()) == len(rhs.getChildren())
+        count = len(lhs.getChildren())
+        for i, (l, r) in enumerate(zip(lhs.getChildren(), rhs.getChildren())):
+            self.__element2element(l, r, context_info=(i, count))
+
+    def __compare_element_namespace(self, lhs, rhs):
+        """
+        Compares the given elements' namespaces.
+
+        Empty string & None XML element namespaces are considered the same to
+        compensate for the suds SAX document model representing the following
+        'default namespace' scenarios differently:
+          <a/>
+          <a xmlns=""/>
+          <ns:a xmlns:ns=""/>
+
+        """
+        #TODO: Make suds SAX element model consistently represent empty/missing
+        # namespaces and then update both this method and its docstring.
+        self.__push_context("namespace")
+        lhs_namespace = lhs.namespace()[1]
+        rhs_namespace = rhs.namespace()[1]
+        if not lhs_namespace:
+            lhs_namespace = None
+        if not rhs_namespace:
+            rhs_namespace = None
+        assert lhs_namespace == rhs_namespace
+        self.__pop_context()
+
+    def __compare_element_text(self, lhs, rhs):
+        """
+        Compares the given elements' textual content.
+
+        Empty string & None XML element texts are considered the same to
+        compensate for different XML object tree construction methods
+        representing 'no text' elements differently, e.g. depending on whether
+        a particular SAX parsed XML element had any whitespace characters in
+        its textual data or whether the element got constructed in code to
+        represent a SOAP request.
+
+        """
+        #TODO: Make suds SAX element model consistently represent empty/missing
+        # text content and then update both this method and its docstring.
+        self.__push_context("text")
+        lhs_text = lhs.text
+        rhs_text = rhs.text
+        if not lhs_text:
+            lhs_text = None
+        if not rhs_text:
+            rhs_text = None
+        assert lhs_text == rhs_text
+        self.__pop_context()
+
+    def __document2document(self, lhs, rhs, context):
+        """
+        Internal document2document comparison worker.
+
+        See document2document() docstring for more detailed information.
+
+        """
+        self.__push_context(context)
+        assert lhs.__class__ is suds.sax.document.Document
+        assert rhs.__class__ is suds.sax.document.Document
+        self.__compare_child_elements(lhs, rhs)
+        self.__pop_context()
+
+    @staticmethod
+    def __element_name(element):
+        """Returns a given SAX element's name as unicode or '???' on error."""
+        try:
+            return text_type(element.name)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            return u("???")
+
+    def __element2element(self, lhs, rhs, context_info=(0, 1)):
+        """
+        Internal element2element comparison worker.
+
+        See element2element() docstring for more detailed information.
+
+        The context information is an (n, count) 2-element collection
+        indicating which element2element() call ('n') this is in a sequence of
+        such calls ('count'). The exact context string is constructed based on
+        the given elements' names and context information.
+
+        """
+        context = self.__element2element_context(lhs, rhs, context_info)
+        self.__push_context(context)
+        assert lhs.__class__ is suds.sax.element.Element
+        assert rhs.__class__ is suds.sax.element.Element
+        assert lhs.name == rhs.name
+        self.__compare_element_namespace(lhs, rhs)
+        self.__compare_element_text(lhs, rhs)
+        self.__compare_child_elements(lhs, rhs)
+        self.__pop_context()
+
+    @classmethod
+    def __element2element_context(cls, lhs, rhs, context_info):
+        """
+        Return a context string for a given element2element call.
+
+        See the __element2element() docstring for more detailed information.
+
+        """
+        n, count = context_info
+        assert 0 <= n < count, "broken CompareSAX implementation"
+        context_lhs_name = cls.__element_name(lhs)
+        context_rhs_name = cls.__element_name(rhs)
+        if context_lhs_name == context_rhs_name:
+            context_name = context_lhs_name
+        else:
+            context_name = "%s/%s" % (context_lhs_name, context_rhs_name)
+        if count == 1:
+            return "<%s>" % (context_name,)
+        return "<%s(%d/%d)>" % (context_name, n + 1, count)
+
+    @staticmethod
+    def __parse_data(data):
+        """
+        Construct a SAX XML document based on its data given as a string or a
+        bytes object.
+
+        """
+        if isinstance(data, text_type):
+            data = data.encode("utf-8")
+        return suds.sax.parser.Parser().parse(string=data)
+
+    def __pop_context(self):
+        self.__context.pop()
+
+    def __push_context(self, context):
+        self.__context.append(context)
+
+    def __report_context(self):
+        if self.__context:
+            sys.stderr.write("Failed SAX XML comparison context:\n")
+            sys.stderr.write("  %s\n" % (".".join(self.__context)))
diff --git a/tests/indirect_parametrize.py b/tests/testutils/indirect_parametrize.py
similarity index 95%
rename from tests/indirect_parametrize.py
rename to tests/testutils/indirect_parametrize.py
index 4c909c0..a8f1e0f 100644
--- a/tests/indirect_parametrize.py
+++ b/tests/testutils/indirect_parametrize.py
@@ -56,8 +56,8 @@ calculating them from a much shorter definition:
  * test_example("Madagascar", 10)
  * test_example("Madagascar", 20)
  * test_example("Madagascar", 30)
- * test_example("Rumpelstilskin", 20)
- * test_example("Rumpelstilskin", 40)
+ * test_example("Rumpelstiltskin", 20)
+ * test_example("Rumpelstiltskin", 40)
 
 def custom_parametrization(param_names, param_value_defs):
     param_values = []
@@ -69,7 +69,7 @@ def custom_parametrization(param_names, param_value_defs):
 @pytest.indirect_parametrize(custom_parametrization, ("uno", "due"), (
     ("Fritula", (1, 2, 3, 4, 5, 6, 7, 8, 9)),
     ("Madagascar", (10, 30, 50)),
-    ("Rumpelstilskin", (20, 40))))
+    ("Rumpelstiltskin", (20, 40))))
 def test_example(uno, due):
     assert False
 
diff --git a/tools/readme.txt b/tools/readme.txt
new file mode 100644
index 0000000..7bb7d92
--- /dev/null
+++ b/tools/readme.txt
@@ -0,0 +1,8 @@
+Tools & utilities used during suds development.
+
+Only stand-alone scripts should be located in this folder. Any imported
+additional modules should be located under the suds_devel package folder.
+
+All Python scripts under this folder should be prepared so their sources are
+directly compatible with both Python 2.x & 3.x without the need for any py2to3
+based source code transformation.
diff --git a/tools/run_all_tests.py b/tools/run_all_tests.py
new file mode 100644
index 0000000..d77e5ca
--- /dev/null
+++ b/tools/run_all_tests.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetiæ ( jurko.gospodnetic at pke.hr )
+
+"""
+"poor man's tox" development script used on Windows to run the full suds-jurko
+test suite using multiple Python interpreter versions.
+
+Intended to be used as a general 'all tests passed' check. To see more detailed
+information on specific failures, run the failed test group manually,
+configured for greater verbosity than done here.
+
+"""
+
+import os.path
+import shutil
+import sys
+
+from suds_devel.configuration import BadConfiguration, Config, configparser
+from suds_devel.environment import BadEnvironment
+import suds_devel.utility as utility
+
+
+class MyConfig(Config):
+
+    def __init__(self, script, project_folder, ini_file):
+        """
+        Initialize new script configuration.
+
+        External configuration parameters may be specified relative to the
+        following folders:
+          * script - relative to the current working folder
+          * project_folder - relative to the script folder
+          * ini_file - relative to the project folder
+
+        """
+        super(MyConfig, self).__init__(script, project_folder, ini_file)
+        try:
+            self._read_environment_configuration()
+        except configparser.Error:
+            raise BadConfiguration(sys.exc_info()[1].message)
+
+
+def _prepare_configuration():
+    # We know we are a regular stand-alone script file and not an imported
+    # module (either frozen, imported from disk, zip-file, external database or
+    # any other source). That means we can safely assume we have the __file__
+    # attribute available.
+    global config
+    config = MyConfig(__file__, "..", "setup.cfg")
+
+
+def _print_title(env, message_fmt):
+    separator = "-" * 63
+    print("")
+    print(separator)
+    print("--- " + message_fmt % (env.name(),))
+    print(separator)
+
+
+def _report_startup_information():
+    print("Running in folder: '%s'" % (os.getcwd(),))
+
+
+def _run_tests(env):
+    if env.sys_version_info >= (3,):
+        _print_title(env, "Building suds for Python %s")
+        build_folder = os.path.join(config.project_folder, "build")
+        if os.path.isdir(build_folder):
+            shutil.rmtree(build_folder)
+
+    # Install the project into the target Python environment in editable mode.
+    # This will actually build Python 3 sources in case we are using a Python 3
+    # environment.
+    setup_cmd = ["setup.py", "-q", "develop"]
+    _, _, return_code = env.execute(setup_cmd, cwd=config.project_folder)
+    if return_code != 0:
+        return False
+
+    test_folder = os.path.join(config.project_folder, "tests")
+    pytest_cmd = ["-m", "pytest", "-q", "-x", "--tb=short"]
+
+    _print_title(env, "Testing suds with Python %s")
+    _, _, return_code = env.execute(pytest_cmd, cwd=test_folder)
+    if return_code != 0:
+        return False
+
+    _print_title(env, "Testing suds with Python %s - no assertions")
+    pytest_cmd.insert(0, "-O")
+    _, _, return_code = env.execute(pytest_cmd, cwd=test_folder)
+    return return_code == 0
+
+
+def _run_tests_in_all_environments():
+    if not config.python_environments:
+        raise BadConfiguration("No Python environments configured.")
+    for env in config.python_environments:
+        if not env.initial_scan_completed:
+            _print_title(env, "Scanning environment Python %s")
+            env.run_initial_scan()
+        if not _run_tests(env):
+            return False
+    return True
+
+
+def main():
+    try:
+        _report_startup_information()
+        _prepare_configuration()
+        success = _run_tests_in_all_environments()
+    except (BadConfiguration, BadEnvironment):
+        utility.report_error(sys.exc_info()[1])
+        return -2
+    print("")
+    if not success:
+        print("Test failed.")
+        return -3
+    print("All tests passed.")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tools/setup_base_environments.py b/tools/setup_base_environments.py
new file mode 100644
index 0000000..d6af600
--- /dev/null
+++ b/tools/setup_base_environments.py
@@ -0,0 +1,981 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Sets up base Python development environments used by this project.
+
+These are the Python environments from which multiple virtual environments can
+then be spawned as needed.
+
+The environments should have the following Python packages installed:
+  * setuptools (for installing pip)
+  * pip (for installing everything except itself)
+  * pytest (for running the project's test suite)
+  * six (Python 2/3 compatibility layer used in the project's test suite)
+  * virtualenv (for creating virtual Python environments)
+plus certain specific Python versions may require additional backward
+compatibility support packages.
+
+"""
+#TODO: Python 3.4.0 comes with setuptools & pip preinstalled but they can be
+# installed and/or upgraded manually if needed so we choose not to use their
+# built-in installations, i.e. the built-in ensurepip module. Consider using
+# the built-in ensurepip module if regular setuptools/pip installation fails
+# for some reason or has been configured to run locally.
+#TODO: logging
+#TODO: command-line option support
+#TODO: support for additional configuration files, e.g. ones that are developer
+# or development environment specific.
+#TODO: warn if multiple environments use the same executable
+#TODO: make the script importable
+#TODO: report when the installed package version is newer than the last tested
+# one
+#TODO: hold a list of last tested package versions and report a warning if a
+# newer one is encountered
+#  > # Where no Python package version has been explicitly specified, the
+#  > # following 'currently latest available' package release has been
+#  > # successfully used:
+#  > "last tested package version": {
+#  >     "argparse": "1.2.1",
+#  >     "backports.ssl_match_hostname": "3.4.0.2",
+#  >     "colorama": "0.3.1",
+#  >     "pip": "1.5.5",
+#  >     "py": "1.4.20",
+#  >     "pytest": "2.5.2",
+#  >     "setuptools": "3.6",
+#  >     "virtualenv": "1.11.5"},
+#TODO: automated checking for new used package versions, e.g. using PyPI XRC
+# API:
+#  > import xmlrpc.client as xrc
+#  > client = xrc.ServerProxy("http://pypi.python.org/pypi")
+#  > client.package_releases("six")  # just the latest release
+#  > client.package_releases("six", True)  # all releases
+#TODO: option to break on bad environments
+#TODO: verbose option to report bad environment detection output
+#TODO: verbose option to report all environment detection output
+#TODO: verbose option to report environment details
+#TODO: Consider running the environment scanner script from an empty temporary
+# folder to avoid importing random same-named modules from the current working
+# folder. An alternative would be to play around with sys.path in the scanner
+# script, e.g. remove its first element. Also, we might want to clear out any
+# globally set Python environment variables such as PYTHONPATH.
+#TODO: collect stdout, stderr & log outputs for each easy_install/pip run
+#TODO: configurable - avoid downloads if a suitable locally downloaded source
+# is available (ez_setup, setuptools, pip)
+#TODO: 244 downloads must come before 243 installation
+#TODO: support configuring what latest suitable version we want to use if
+# available in the following layers:
+#  - already installed
+#  - local installation cache (can not find out the latest available content,
+#    can only download to it or install from it)
+#  - pypi
+# related configuration options:
+#  - allow already installed,
+#  - allow locally downloaded,
+#  - allow pypi
+#TODO: if you want better local-cache support - use devpi:
+#  - better caching support and version detection
+#  - devpi & pypi usage transparent
+#  - we'll need a script for installing & setting up the devpi server
+#TODO: parallelization
+#TODO: concurrency support - file locking required for:
+# installation cache folder:
+#   downloading (write)
+#   zipping eggs (write)
+#   installing from local folder (read)
+# Python environment installation area:
+#   installing new packages (write)
+#   running Python code (read)
+#TODO: test whether we can upgrade pip in-place
+#TODO: test how we can make pip safe to use when there are multiple pip based
+# installations being run at the same time by the same user - specify some
+# global folders, like the temp build folder, explicitly
+#TODO: Detect most recent packages on PyPI, but do that at most once in a
+# single script run, or with a separate script, or use devpi. Currently, if a
+# suitable package is found locally, a more suitable one will not be checked
+# for on PyPI.
+#TODO: Recheck error handling to make sure all failed commands are correctly
+# detected. Some might not set a non-0 exit code on error and so their output
+# must be used as a success/failure indicator.
+
+import itertools
+import os
+import os.path
+import re
+import sys
+import tempfile
+
+from suds_devel.configuration import BadConfiguration, Config, configparser
+from suds_devel.egg import zip_eggs_in_folder
+from suds_devel.environment import BadEnvironment, Environment
+from suds_devel.exception import EnvironmentSetupError
+from suds_devel.parse_version import parse_version
+from suds_devel.requirements import (pytest_requirements, six_requirements,
+    virtualenv_requirements)
+import suds_devel.utility as utility
+
+
+# -------------
+# Configuration
+# -------------
+
+class MyConfig(Config):
+
+    # Section names.
+    SECTION__ACTIONS = "setup base environments - actions"
+    SECTION__FOLDERS = "setup base environments - folders"
+    SECTION__REUSE_PREINSTALLED_SETUPTOOLS =  \
+        "setup base environments - reuse pre-installed setuptools"
+
+    def __init__(self, script, project_folder, ini_file):
+        """
+        Initialize new script configuration.
+
+        External configuration parameters may be specified relative to the
+        following folders:
+          * script - relative to the current working folder
+          * project_folder - relative to the script folder
+          * ini_file - relative to the project folder
+
+        """
+        super(MyConfig, self).__init__(script, project_folder, ini_file)
+        self.__cached_paths = {}
+        try:
+            self.__read_configuration()
+        except configparser.Error:
+            raise BadConfiguration(sys.exc_info()[1].message)
+
+    def ez_setup_folder(self):
+        return self.__get_cached_path("ez_setup folder")
+
+    def installation_cache_folder(self):
+        return self.__get_cached_path("installation cache")
+
+    def pip_download_cache_folder(self):
+        return self.__get_cached_path("pip download cache")
+
+    def __get_cached_path(self, option):
+        try:
+            return self.__cached_paths[option]
+        except KeyError:
+            x = self.__cached_paths[option] = self.__get_path(option)
+            return x
+
+    def __get_path(self, option):
+        try:
+            folder = self._reader.get(self.SECTION__FOLDERS, option)
+        except (configparser.NoOptionError, configparser.NoSectionError):
+            return
+        except configparser.Error:
+            raise BadConfiguration("Error reading configuration option "
+                "'%s.%s' - %s" % (self.SECTION__FOLDERS, option,
+                sys.exc_info()[1]))
+        base_paths = {
+            "project-folder": self.project_folder,
+            "script-folder": self.script_folder,
+            "ini-folder": os.path.dirname(self.ini_file)}
+        folder_parts = re.split("[\\/]{2}", folder, maxsplit=1)
+        base_path = None
+        if len(folder_parts) == 2:
+            base_path = base_paths.get(folder_parts[0].lower())
+            if base_path is not None:
+                folder = folder_parts[1]
+        if not folder:
+            raise BadConfiguration("Configuration option '%s.%s' invalid. A "
+                "valid relative path must not be empty. Use '.' to represent "
+                "the base folder." % (section, option))
+        if base_path is None:
+            base_path = base_paths.get("ini-folder")
+        return os.path.normpath(os.path.join(base_path, folder))
+
+    def __read_configuration(self):
+        self._read_environment_configuration()
+
+        section = self.SECTION__REUSE_PREINSTALLED_SETUPTOOLS
+        self.reuse_old_setuptools = self._get_bool(section, "old")
+        self.reuse_best_setuptools = self._get_bool(section, "best")
+        self.reuse_future_setuptools = self._get_bool(section, "future")
+
+        section = self.SECTION__ACTIONS
+        self.report_environment_configuration = (
+            self._get_bool(section, "report environment configuration"))
+        self.report_raw_environment_scan_results = (
+            self._get_bool(section, "report raw environment scan results"))
+        self.setup_setuptools = (
+            self._get_tribool(section, "setup setuptools"))
+        self.download_installations = (
+            self._get_tribool(section, "download installations"))
+        self.install_environments = (
+            self._get_tribool(section, "install environments"))
+
+
+def _prepare_configuration():
+    # We know we are a regular stand-alone script file and not an imported
+    # module (either frozen, imported from disk, zip-file, external database or
+    # any other source). That means we can safely assume we have the __file__
+    # attribute available.
+    global config
+    config = MyConfig(__file__, "..", "setup.cfg")
+
+
+# --------------------
+# Environment scanning
+# --------------------
+
+def report_environment_configuration(env):
+    if not (env and env.initial_scan_completed):
+        return
+    print("  ctypes version: %s" % (env.ctypes_version,))
+    print("  pip version: %s" % (env.pip_version,))
+    print("  pytest version: %s" % (env.pytest_version,))
+    print("  python version: %s" % (env.python_version,))
+    print("  setuptools version: %s" % (env.setuptools_version,))
+    if env.setuptools_zipped_egg is not None:
+        print("  setuptools zipped egg: %s" % (env.setuptools_zipped_egg,))
+    print("  virtualenv version: %s" % (env.virtualenv_version,))
+
+
+def report_raw_environment_scan_results(out, err, exit_code):
+    if out is None and err is None and exit_code is None:
+        return
+    print("-----------------------------------")
+    print("--- RAW SCAN RESULTS --------------")
+    print("-----------------------------------")
+    if exit_code is not None:
+        print("*** EXIT CODE: %d" % (exit_code,))
+    for name, value in (("STDOUT", out), ("STDERR", err)):
+        if value:
+            print("*** %s:" % (name,))
+            sys.stdout.write(value)
+            if value[-1] != "\n":
+                sys.stdout.write("\n")
+    print("-----------------------------------")
+
+
+class ScanProgressReporter:
+    """
+    Reports scanning progress to the user.
+
+    Takes care of all the gory progress output formatting details so they do
+    not pollute the actual scanning logic implementation.
+
+    A ScanProgressReporter's output formatting logic assumes that the reporter
+    is the one with full output control between calls to a its report_start() &
+    report_finish() methods. Therefore, user code must not do any custom output
+    during that time or it risks messing up the reporter's output formatting.
+
+    """
+
+    def __init__(self, environments):
+        self.__max_name_length = max(len(x.name()) for x in environments)
+        self.__count = len(environments)
+        self.__count_width = len(str(self.__count))
+        self.__current = 0
+        self.__reporting = False
+        print("Scanning Python environments...")
+
+    def report_start(self, name):
+        assert len(name) <= self.__max_name_length
+        assert self.__current <= self.__count
+        assert not self.__reporting
+        self.__reporting = True
+        self.__current += 1
+        name_padding = " " * (self.__max_name_length - len(name))
+        sys.stdout.write("[%*d/%d] Scanning '%s'%s - " % (self.__count_width,
+            self.__current, self.__count, name, name_padding))
+        sys.stdout.flush()
+
+    def report_finish(self, report):
+        assert self.__reporting
+        self.__reporting = False
+        print(report)
+
+
+class ScannedEnvironmentTracker:
+    """Helps track scanned Python environments and report duplicates."""
+
+    def __init__(self):
+        self.__names = set()
+        self.__last_name = None
+        self.__environments = []
+
+    def environments(self):
+        return self.__environments
+
+    def track_environment(self, env):
+        assert env not in self.__environments
+        assert env.name() == self.__last_name
+        self.__environments.append(env)
+
+    def track_name(self, name):
+        if name in self.__names:
+            raise BadConfiguration("Python environment '%s' configured "
+                "multiple times." % (name,))
+        self.__names.add(name)
+        self.__last_name = name
+
+
+def scan_python_environment(env, progress_reporter, environment_tracker):
+    environment_tracker.track_name(env.name())
+    # N.B. No custom output allowed between calls to our progress_reporter's
+    # report_start() & report_finish() methods or we risk messing up its output
+    # formatting.
+    progress_reporter.report_start(env.name())
+    try:
+        try:
+            out, err, exit_code = env.run_initial_scan()
+        except:
+            progress_reporter.report_finish("----- %s" % (_exc_str(),))
+            raise
+    except BadEnvironment:
+        out, err, exit_code = sys.exc_info()[1].raw_scan_results()
+    else:
+        progress_reporter.report_finish(env.description())
+        environment_tracker.track_environment(env)
+    if config.report_raw_environment_scan_results:
+        report_raw_environment_scan_results(out, err, exit_code)
+    if config.report_environment_configuration:
+        report_environment_configuration(env)
+
+
+def scan_python_environments():
+    environments = config.python_environments
+    if not environments:
+        raise BadConfiguration("No Python environments configured.")
+    progress_reporter = ScanProgressReporter(environments)
+    environment_tracker = ScannedEnvironmentTracker()
+    for env in environments:
+        scan_python_environment(env, progress_reporter, environment_tracker)
+    return environment_tracker.environments()
+
+
+# ------------------------------------------
+# Generic functionality local to this module
+# ------------------------------------------
+
+def _create_installation_cache_folder_if_needed():
+    assert config.installation_cache_folder() is not None
+    if not os.path.isdir(config.installation_cache_folder()):
+        print("Creating installation cache folder...")
+        # os.path.abspath() to avoid ".." entries in the path that would
+        # otherwise confuse os.makedirs().
+        os.makedirs(os.path.abspath(config.installation_cache_folder()))
+
+
+def _exc_str():
+    exc_type, exc = sys.exc_info()[:2]
+    type_desc = []
+    if exc_type.__module__ and exc_type.__module__ != "__main__":
+        type_desc.append(exc_type.__module__)
+    type_desc.append(exc_type.__name__)
+    desc = ".".join(type_desc), str(exc)
+    return ": ".join(x for x in desc if x)
+
+
+def _report_configuration():
+    folder = config.installation_cache_folder()
+    if folder is not None:
+        print("Installation cache folder: '%s'" % (folder,))
+    folder = config.pip_download_cache_folder()
+    if folder is not None:
+        print("PIP download cache folder: '%s'" % (folder,))
+
+
+def _report_startup_information():
+    print("Running in folder: '%s'" % (os.getcwd(),))
+
+
+# ----------------------------------
+# Processing setuptools installation
+# ----------------------------------
+
+def process_setuptools(env, actions):
+    if "setup setuptools" not in actions:
+        return
+    installer = _ez_setup_script(env)
+    if _reuse_pre_installed_setuptools(env, installer):
+        return
+    _avoid_setuptools_zipped_egg_upgrade_issue(env, installer)
+    try:
+        # 'ez_setup' script will download its setuptools installation to the
+        # 'current working folder'. If we are using an installation cache
+        # folder, we run the script from there to get the downloaded setuptools
+        # installation stored together with all of the other used
+        # installations. If we are not, then just have it downloaded to the
+        # current folder.
+        if config.installation_cache_folder() is not None:
+            _create_installation_cache_folder_if_needed()
+        installer.execute(cwd=config.installation_cache_folder())
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        raise EnvironmentSetupError("setuptools installation failed - %s" % (
+            _exc_str(),))
+
+
+class _ez_setup_script:
+    """setuptools project's ez_setup installer script."""
+
+    def __init__(self, env):
+        self.__env = env
+        if not config.ez_setup_folder():
+            self.__error("ez_setup folder not configured")
+        self.__ez_setup_folder = config.ez_setup_folder()
+        self.__cached_script_path = None
+        self.__cached_setuptools_version = None
+        if not os.path.isfile(self.script_path()):
+            self.__error("installation script '%s' not found" % (
+                self.script_path(),))
+
+    def execute(self, cwd=None):
+        script_path = self.script_path()
+        kwargs = {}
+        if cwd:
+            kwargs["cwd"] = cwd
+            script_path = os.path.abspath(script_path)
+        self.__env.execute([script_path], **kwargs)
+
+    def script_path(self):
+        if self.__cached_script_path is None:
+            self.__cached_script_path = self.__script_path()
+        return self.__cached_script_path
+
+    def setuptools_version(self):
+        if self.__cached_setuptools_version is None:
+            self.__cached_setuptools_version = self.__setuptools_version()
+        return self.__cached_setuptools_version
+
+    def __error(self, msg):
+        raise EnvironmentSetupError("Can not install setuptools - %s." % (
+            msg,))
+
+    def __script_path(self):
+        import suds_devel.ez_setup_versioned as ez
+        script_name = ez.script_name(self.__env.sys_version_info)
+        return os.path.join(self.__ez_setup_folder, script_name)
+
+    def __setuptools_version(self):
+        """Read setuptools version from the underlying ez_setup script."""
+        # Read the script directly as a file instead of importing it as a
+        # Python module and reading the value from the loaded module's global
+        # DEFAULT_VERSION variable. Not all ez_setup scripts are compatible
+        # with all Python environments and so importing them would require
+        # doing so using a separate process run in the target Python
+        # environment instead of the current one.
+        f = open(self.script_path(), "r")
+        try:
+            matcher = re.compile(r'\s*DEFAULT_VERSION\s*=\s*"([^"]*)"\s*$')
+            for i, line in enumerate(f):
+                if i > 50:
+                    break
+                match = matcher.match(line)
+                if match:
+                    return match.group(1)
+        finally:
+            f.close()
+        self.__error("error parsing setuptools installation script '%s'" % (
+            self.script_path(),))
+
+
+def _avoid_setuptools_zipped_egg_upgrade_issue(env, ez_setup):
+    """
+    Avoid the setuptools self-upgrade issue.
+
+    setuptools versions prior to version 3.5.2 have a bug that can cause their
+    upgrade installations to fail when installing a new zipped egg distribution
+    over an existing zipped egg setuptools distribution with the same name.
+
+    The following Python versions are not affected by this issue:
+      Python 2.4 - use setuptools 1.4.2 - installs itself as a non-zipped egg
+      Python 2.6+ - use setuptools versions not affected by this issue
+    That just leaves Python versions 2.5.x to worry about.
+
+    This problem occurs because of an internal stale cache issue causing the
+    upgrade to read data from the new zip archive at a location calculated
+    based on the original zip archive's content, effectively causing such read
+    operations to either succeed (if read content had not changed its
+    location), fail with a 'bad local header' exception or even fail silently
+    and return incorrect data.
+
+    To avoid the issue, we explicitly uninstall the previously installed
+    setuptools distribution before installing its new version.
+
+    """
+    if env.sys_version_info[:2] != (2, 5):
+        return  # only Python 2.5.x affected by this
+    if not env.setuptools_zipped_egg:
+        return  # setuptools not pre-installed as a zipped egg
+    pv_new = parse_version(ez_setup.setuptools_version())
+    if pv_new != parse_version(env.setuptools_version):
+        return  # issue avoided since zipped egg archive names will not match
+    fixed_version = utility.lowest_version_string_with_prefix("3.5.2")
+    if pv_new >= parse_version(fixed_version):
+        return  # issue fixed in setuptools
+    # We could check for pip and use it for a cleaner setuptools uninstall if
+    # available, but YAGNI since only Python 2.5.x environments are affected by
+    # the zipped egg upgrade issue.
+    os.remove(env.setuptools_zipped_egg)
+
+
+def _reuse_pre_installed_setuptools(env, installer):
+    """
+    Return whether a pre-installed setuptools distribution should be reused.
+
+    """
+    if not env.setuptools_version:
+        return  # no prior setuptools ==> no reuse
+    reuse_old = config.reuse_old_setuptools
+    reuse_best = config.reuse_best_setuptools
+    reuse_future = config.reuse_future_setuptools
+    reuse_comment = None
+    if reuse_old or reuse_best or reuse_future:
+        pv_old = parse_version(env.setuptools_version)
+        pv_new = parse_version(installer.setuptools_version())
+        if pv_old < pv_new:
+            if reuse_old:
+                reuse_comment = "%s+ recommended" % (
+                    installer.setuptools_version(),)
+        elif pv_old > pv_new:
+            if reuse_future:
+                reuse_comment = "%s+ required" % (
+                    installer.setuptools_version(),)
+        elif reuse_best:
+            reuse_comment = ""
+    if reuse_comment is None:
+        return  # reuse not allowed by configuration
+    if reuse_comment:
+        reuse_comment = " (%s)" % (reuse_comment,)
+    print("Reusing pre-installed setuptools %s distribution%s." % (
+        env.setuptools_version, reuse_comment))
+    return True  # reusing pre-installed setuptools
+
+
+# ---------------------------
+# Processing pip installation
+# ---------------------------
+
+def calculate_pip_requirements(env_version_info):
+    # pip releases supported on older Python versions:
+    #   * Python 2.4.x - pip 1.1.
+    #   * Python 2.5.x - pip 1.3.1.
+    pip_version = None
+    if env_version_info < (2, 5):
+        pip_version = "1.1"
+    elif env_version_info < (2, 6):
+        pip_version = "1.3.1"
+    requirement_spec = utility.requirement_spec
+    requirements = [requirement_spec("pip", pip_version)]
+    # Although pip claims to be compatible with Python 3.0 & 3.1 it does not
+    # seem to work correctly from within such clean Python environments.
+    #   * Tested using pip 1.5.4 & Python 3.1.3.
+    #   * pip can be installed using Python 3.1.3 ('py313 -m easy_install pip')
+    #     but attempting to use it or even just import its pip Python module
+    #     fails.
+    #   * The problem is caused by a bug in pip's backward compatibility
+    #     support implementation, but can be worked around by installing the
+    #     backports.ssl_match_hostname package from PyPI.
+    if (3,) <= env_version_info < (3, 2):
+        requirements.append(requirement_spec("backports.ssl_match_hostname"))
+    return requirements
+
+
+def download_pip(env, requirements):
+    """Download pip and its requirements using setuptools."""
+    if config.installation_cache_folder() is None:
+        raise EnvironmentSetupError("Local installation cache folder not "
+            "defined but required for downloading a pip installation.")
+    # Installation cache folder needs to be explicitly created for setuptools
+    # to be able to copy its downloaded installation files into it. Seen using
+    # Python 2.4.4 & setuptools 1.4.
+    _create_installation_cache_folder_if_needed()
+    try:
+        env.execute(["-m", "easy_install", "--zip-ok", "--multi-version",
+            "--always-copy", "--exclude-scripts", "--install-dir",
+            config.installation_cache_folder()] + requirements)
+        zip_eggs_in_folder(config.installation_cache_folder())
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        raise EnvironmentSetupError("pip download failed.")
+
+
+def setuptools_install_options(local_storage_folder):
+    """
+    Return options to make setuptools use installations from the given folder.
+
+    No other installation source is allowed.
+
+    """
+    if local_storage_folder is None:
+        return []
+    # setuptools expects its find-links parameter to contain a list of link
+    # sources (either local paths, file: URLs pointing to folders or URLs
+    # pointing to a file containing HTML links) separated by spaces. That means
+    # that, when specifying such items, whether local paths or URLs, they must
+    # not contain spaces. The problem can be worked around by using a local
+    # file URL, since URLs can contain space characters encoded as '%20' (for
+    # more detailed information see below).
+    #
+    # Any URL referencing a folder needs to be specified with a trailing '/'
+    # character in order for setuptools to correctly recognize it as a folder.
+    #
+    # All this has been tested using Python 2.4.3/2.4.4 & setuptools 1.4/1.4.2
+    # as well as Python 3.4 & setuptools 3.3.
+    #
+    # Supporting paths with spaces - method 1:
+    # ----------------------------------------
+    # One way would be to prepare a link file and pass an URL referring to that
+    # link file. The link file needs to contain a list of HTML link tags
+    # (<a href="..."/>), one for every item stored inside the local storage
+    # folder. If a link file references a folder whose name matches the desired
+    # requirement name, it will be searched recursively (as described in method
+    # 2 below).
+    #
+    # Note that in order for setuptools to recognize a local link file URL
+    # correctly, the file needs to be named with the '.html' extension. That
+    # will cause the underlying urllib2.open() operation to return the link
+    # file's content type as 'text/html' which is required for setuptools to
+    # recognize a valid link file.
+    #
+    # Supporting paths with spaces - method 2:
+    # ----------------------------------------
+    # Another possible way is to use an URL referring to the local storage
+    # folder directly. This will cause setuptools to prepare and use a link
+    # file internally - with its content read from a 'index.html' file located
+    # in the given local storage folder, if it exists, or constructed so it
+    # contains HTML links to all top-level local storage folder items, as
+    # described for method 1 above.
+    if " " in local_storage_folder:
+        find_links_param = utility.path_to_URL(local_storage_folder)
+        if find_links_param[-1] != "/":
+            find_links_param += "/"
+    else:
+        find_links_param = local_storage_folder
+    return ["-f", find_links_param, "--allow-hosts=None"]
+
+
+def install_pip(env, requirements):
+    """Install pip and its requirements using setuptools."""
+    try:
+        installation_source_folder = config.installation_cache_folder()
+        options = setuptools_install_options(installation_source_folder)
+        if installation_source_folder is not None:
+            zip_eggs_in_folder(installation_source_folder)
+        env.execute(["-m", "easy_install"] + options + requirements)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        raise EnvironmentSetupError("pip installation failed.")
+
+
+def process_pip(env, actions):
+    download = "download pip installation" in actions
+    install = "run pip installation" in actions
+    if not download and not install:
+        return
+    requirements = calculate_pip_requirements(env.sys_version_info)
+    if download:
+        download_pip(env, requirements)
+    if install:
+        install_pip(env, requirements)
+
+
+# ----------------------------------
+# Processing pip based installations
+# ----------------------------------
+
+def pip_invocation_arguments(env_version_info):
+    """
+    Returns Python arguments for invoking pip with a specific Python version.
+
+    Running pip based installations on Python prior to 2.7.
+      * pip based installations may be run using:
+          python -c "import pip;pip.main()" install <package-name-to-install>
+        in addition to the regular command:
+          python -m pip install <package-name-to-install>
+      * The '-m' option can not be used with certain Python versions prior to
+        Python 2.7.
+          * Whether this is so also depends on the specific pip version used.
+          * Seems to not work with Python 2.4 and pip 1.1.
+          * Seems to work fine with Python 2.5.4 and pip 1.3.1.
+          * Seems to not work with Python 2.6.6 and pip 1.5.4.
+
+    """
+    if (env_version_info < (2, 5)) or ((2, 6) <= env_version_info < (2, 7)):
+        return ["-c", "import pip;pip.main()"]
+    return ["-m", "pip"]
+
+
+def pip_requirements_file(requirements):
+    janitor = None
+    try:
+        os_handle, file_path = tempfile.mkstemp(suffix=".pip-requirements",
+            text=True)
+        requirements_file = os.fdopen(os_handle, "w")
+        try:
+            janitor = utility.FileJanitor(file_path)
+            for line in requirements:
+                requirements_file.write(line)
+                requirements_file.write("\n")
+        finally:
+            requirements_file.close()
+        return file_path, janitor
+    except:
+        if janitor:
+            janitor.clean()
+        raise
+
+
+def prepare_pip_requirements_file_if_needed(requirements):
+    """
+    Make requirements be passed to pip via a requirements file if needed.
+
+    We must be careful about how we pass shell operator characters (e.g. '<',
+    '>', '|' or '^') included in our command-line arguments or they might cause
+    problems if run through an intermediate shell interpreter. If our pip
+    requirement specifications contain such characters, we pass them using a
+    separate requirements file.
+
+    This problem has been encountered on Windows 7 SP1 x64 using Python 2.4.3,
+    2.4.4 & 2.5.4.
+
+    """
+    if utility.any_contains_any(requirements, "<>|()&^"):
+        file_path, janitor = pip_requirements_file(requirements)
+        requirements[:] = ["-r", file_path]
+        return janitor
+
+
+def prepare_pip_requirements(env):
+    requirements = list(itertools.chain(
+        pytest_requirements(env.sys_version_info, env.ctypes_version),
+        six_requirements(env.sys_version_info),
+        virtualenv_requirements(env.sys_version_info)))
+    janitor = prepare_pip_requirements_file_if_needed(requirements)
+    return requirements, janitor
+
+
+def pip_download_cache_options(download_cache_folder):
+    if download_cache_folder is None:
+        return []
+    return ["--download-cache=" + download_cache_folder]
+
+
+def download_pip_based_installations(env, pip_invocation, requirements,
+        download_cache_folder):
+    """Download requirements for pip based installation."""
+    if config.installation_cache_folder() is None:
+        raise EnvironmentSetupError("Local installation cache folder not "
+            "defined but required for downloading pip based installations.")
+    # Installation cache folder needs to be explicitly created for pip to be
+    # able to copy its downloaded installation files into it. The same does not
+    # hold for pip's download cache folder which gets created by pip on-demand.
+    # Seen using Python 3.4.0 & pip 1.5.4.
+    _create_installation_cache_folder_if_needed()
+    try:
+        pip_options = ["install", "-d", config.installation_cache_folder(),
+            "--exists-action=i"]
+        pip_options.extend(pip_download_cache_options(download_cache_folder))
+        # Running pip based installations on Python 2.5.
+        #   * Python 2.5 does not come with SSL support enabled by default and
+        #     so pip can not use SSL certified downloads from PyPI.
+        #   * To work around this either install the
+        #     https://pypi.python.org/pypi/ssl package or run pip using the
+        #     '--insecure' command-line options.
+        #       * Installing the ssl package seems ridden with problems on
+        #         Python 2.5 so this workaround has not been tested.
+        if (2, 5) <= env.sys_version_info < (2, 6):
+            # There are some potential cases where we do not need to use
+            # "--insecure", e.g. if the target Python environment already has
+            # the 'ssl' module installed. However, detecting whether this is so
+            # does not seem to be worth the effort. The only way to detect
+            # whether secure download is supported would be to scan the target
+            # environment for this information, e.g. setuptools has this
+            # information in its pip.backwardcompat.ssl variable - if it is
+            # None, the necessary SSL support is not available. But then we
+            # would have to be careful:
+            #  - not to run the scan if we already know this information from
+            #    some previous scan
+            #  - to track all actions that could have invalidated our previous
+            #    scan results, etc.
+            # It just does not seem to be worth the hassle so for now - YAGNI.
+            pip_options.append("--insecure")
+        env.execute(pip_invocation + pip_options + requirements)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        raise EnvironmentSetupError("pip based download failed.")
+
+
+def run_pip_based_installations(env, pip_invocation, requirements,
+        download_cache_folder):
+    # 'pip' download caching system usage notes:
+    # 1. When not installing from our own local installation storage folder, we
+    #    can still use pip's internal download caching system.
+    # 2. We must not enable pip's internal download caching system when
+    #    installing from our own local installation storage folder. In that
+    #    case, pip attempts to populate its cache from our local installation
+    #    folder, but that logic fails when our folder contains a wheel (.whl)
+    #    distribution. More precisely, it fails attempting to store the wheel
+    #    distribution file's content type information. Tested using Python
+    #    3.4.0 & pip 1.5.4.
+    try:
+        pip_options = ["install"]
+        if config.installation_cache_folder() is None:
+            pip_options.extend(pip_download_cache_options(
+                download_cache_folder))
+        else:
+            # pip allows us to identify a local folder containing predownloaded
+            # installation packages using its '-f' command-line option taking
+            # an URL parameter. However, it does not require the URL to be
+            # URL-quoted and it does not even seem to recognize URLs containing
+            # %xx escaped characters. Tested using an installation cache folder
+            # path containing spaces with Python 3.4.0 & pip 1.5.4.
+            installation_cache_folder_URL = utility.path_to_URL(
+                config.installation_cache_folder(), escape=False)
+            pip_options.extend(["-f", installation_cache_folder_URL,
+                "--no-index"])
+        env.execute(pip_invocation + pip_options + requirements)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        raise EnvironmentSetupError("pip based installation failed.")
+
+
+def post_pip_based_installation_fixups(env):
+    """Apply simple post-installation fixes for pip installed packages."""
+    if env.sys_version_info[:2] == (3, 1):
+        from suds_devel.patch_pytest_on_python_31 import patch
+        patch(env)
+
+
+def process_pip_based_installations(env, actions, download_cache_folder):
+    download = "download pip based installations" in actions
+    install = "run pip based installations" in actions
+    if not download and not install:
+        return
+    pip_invocation = pip_invocation_arguments(env.sys_version_info)
+    janitor = None
+    try:
+        requirements, janitor = prepare_pip_requirements(env)
+        if download:
+            download_pip_based_installations(env, pip_invocation, requirements,
+                download_cache_folder)
+        if install:
+            run_pip_based_installations(env, pip_invocation, requirements,
+                download_cache_folder)
+            post_pip_based_installation_fixups(env)
+    finally:
+        if janitor:
+            janitor.clean()
+
+
+# ------------------------------
+# Processing Python environments
+# ------------------------------
+
+def enabled_actions_for_env(env):
+    """Returns actions to perform when processing the given environment."""
+    def enabled(config_value, required):
+        if config_value is Config.TriBool.No:
+            return False
+        if config_value is Config.TriBool.Yes:
+            return True
+        assert config_value is Config.TriBool.IfNeeded
+        return bool(required)
+
+    # Some old Python versions do not support HTTPS downloads and therefore can
+    # not download installation packages from PyPI. To run setuptools or pip
+    # based installations on such Python versions, all the required
+    # installation packages need to be downloaded locally first using a
+    # compatible Python version (e.g. Python 2.4.4 for Python 2.4.3) and then
+    # installed locally.
+    download_supported = not ((2, 4, 3) <= env.sys_version_info < (2, 4, 4))
+
+    local_install = config.installation_cache_folder() is not None
+
+    actions = set()
+
+    pip_required = False
+    run_pip_based_installations = enabled(config.install_environments, True)
+    if run_pip_based_installations:
+        actions.add("run pip based installations")
+        pip_required = True
+    if download_supported and enabled(config.download_installations,
+            local_install and run_pip_based_installations):
+        actions.add("download pip based installations")
+        pip_required = True
+
+    setuptools_required = False
+    run_pip_installation = enabled(config.install_environments, pip_required)
+    if run_pip_installation:
+        actions.add("run pip installation")
+        setuptools_required = True
+    if download_supported and enabled(config.download_installations,
+            local_install and run_pip_installation):
+        actions.add("download pip installation")
+        setuptools_required = True
+
+    if enabled(config.setup_setuptools, setuptools_required):
+        actions.add("setup setuptools")
+
+    return actions
+
+
+def print_environment_processing_title(env):
+    title_length = 73
+    print("-" * title_length)
+    title = "--- %s - Python %s " % (env.name(), env.python_version)
+    title += "-" * max(0, title_length - len(title))
+    print(title)
+    print("-" * title_length)
+
+
+def process_Python_environment(env):
+    actions = enabled_actions_for_env(env)
+    if not actions:
+        return
+    print_environment_processing_title(env)
+    process_setuptools(env, actions)
+    process_pip(env, actions)
+    process_pip_based_installations(env, actions,
+        config.pip_download_cache_folder())
+
+
+def process_python_environments(python_environments):
+    for env in python_environments:
+        try:
+            process_Python_environment(env)
+        except EnvironmentSetupError:
+            utility.report_error(sys.exc_info()[1])
+
+
+def main():
+    try:
+        _report_startup_information()
+        _prepare_configuration()
+        _report_configuration()
+        python_environments = scan_python_environments()
+    except BadConfiguration:
+        utility.report_error(sys.exc_info()[1])
+        return -2
+    process_python_environments(python_environments)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tests/external/jasper.py b/tools/suds_devel/__init__.py
similarity index 52%
copy from tests/external/jasper.py
copy to tools/suds_devel/__init__.py
index fde34d5..9c5f688 100644
--- a/tests/external/jasper.py
+++ b/tools/suds_devel/__init__.py
@@ -1,47 +1,22 @@
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the (LGPL) GNU Lesser General Public License as published by the
-# Free Software Foundation; either version 3 of the License, or (at your
-# option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
-# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software Foundation, Inc.,
-# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-import sys
-sys.path.append('../../')
-
-import traceback as tb
-from tests import *
-from suds import WebFault
-from suds.client import Client
-
-
-errors = 0
-
-
-def start(url):
-    print '\n________________________________________________________________\n'
-    print 'Test @ ( %s )' % url
-
-try:
-    url = 'http://localhost:9090/jasperserver-pro/services/repository?wsdl'
-    start(url)
-    client = Client(url, username='jeff', password='ortel')
-    print client
-    print client.service.list('')
-except WebFault, f:
-    errors += 1
-    print f
-    print f.fault
-except Exception, e:
-    errors += 1
-    print e
-    tb.print_exc()
-
-print '\nFinished: errors = %d' % errors
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetiæ ( jurko.gospodnetic at pke.hr )
+
+"""
+Internal implementation package for different project development utility
+scripts.
+
+"""
diff --git a/tools/suds_devel/configuration.py b/tools/suds_devel/configuration.py
new file mode 100644
index 0000000..0ccf927
--- /dev/null
+++ b/tools/suds_devel/configuration.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Basic configuration support shared in different development utility scripts.
+
+"""
+
+import os.path
+import sys
+
+# Must not use the six Python 2/3 compatibility package from here as this
+# module gets used from the script for setting up basic development
+# environments, and that script needs to be runnable even before the six
+# package has been installed.
+if sys.version_info < (3,):
+    import ConfigParser as configparser
+else:
+    import configparser
+
+from suds_devel.environment import Environment
+import suds_devel.utility as utility
+
+
+class BadConfiguration(Exception):
+    def __init__(self, message):
+        Exception.__init__(self, message)
+
+
+class Config(object):
+
+    # Typed option values.
+    BOOLEAN_TRUE  = ('1', 'yes', 'true', 'on', '+')
+    BOOLEAN_FALSE = ('0', 'no', 'false', 'off', '-')
+    IF_NEEDED     = ('maybe', '?',
+                     'ifneeded', 'if needed',
+                     'asneeded', 'as needed',
+                     'ondemand', 'on demand')
+
+    class TriBool:
+        Yes = object()
+        IfNeeded = object()
+        No = object()
+
+    def __init__(self, script, project_folder, ini_file):
+        """
+        Initialize new script configuration.
+
+        External configuration parameters may be specified relative to the
+        following folders:
+          * script - relative to the current working folder
+          * project_folder - relative to the script folder
+          * ini_file - relative to the project folder
+
+        """
+        self.__init_script_folder(script)
+        self.__init_project_folder(project_folder)
+        self.__init_ini_file(ini_file)
+        self.__init_reader()
+
+    def _get_bool(self, section, option):
+        x = self._reader.get(section, option).lower()
+        if x in self.BOOLEAN_TRUE:
+            return True
+        if x in self.BOOLEAN_FALSE:
+            return False
+        raise BadConfiguration("Option '%s.%s' must be a boolean value." % (
+            section, option))
+
+    def _get_tribool(self, section, option):
+        x = self._reader.get(section, option).lower()
+        if x in self.BOOLEAN_TRUE:
+            return Config.TriBool.Yes
+        if x in self.BOOLEAN_FALSE:
+            return Config.TriBool.No
+        if x in self.IF_NEEDED:
+            return Config.TriBool.IfNeeded
+        raise BadConfiguration("Option '%s.%s' must be Yes, No or IfNeeded." %
+            (section, option))
+
+    def _read_environment_configuration(self):
+        section_prefix = "env:"
+        command_option = "command"
+        self.python_environments = []
+        for section in self._reader.sections():
+            if section.lower().startswith(section_prefix):
+                name = section[len(section_prefix):]
+                command = self._reader.get(section, command_option)
+                if not command:
+                    raise BadConfiguration("'%s.%s' environment command "
+                        "configuration option must not be empty." % (section,
+                        command_option))
+                self.python_environments.append(Environment(name, command))
+
+    def __init_ini_file(self, ini_file):
+        self.ini_file = os.path.join(self.project_folder, ini_file)
+        self.ini_file = os.path.normpath(self.ini_file)
+        if not os.path.isfile(self.ini_file):
+            raise BadConfiguration("Missing configuration file '%s'." % (
+                self.ini_file,))
+
+    def __init_project_folder(self, project_folder):
+        p = os.path.normpath(os.path.join(self.script_folder, project_folder))
+        if not os.path.isdir(p):
+            raise BadConfiguration("Could not find project folder '%s'." % p)
+        self.project_folder = p
+
+    def __init_reader(self):
+        try:
+            f = open(self.ini_file, "r")
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            raise BadConfiguration("Can not access configuration file '%s' - "
+                "%s." % (self.ini_file, sys.exc_info()[1]))
+        try:
+            print("Reading configuration file '%s'..." % (self.ini_file,))
+            self._reader = configparser.ConfigParser()
+            self._reader.readfp(f)
+        finally:
+            f.close()
+
+    def __init_script_folder(self, script):
+        self.script_folder = utility.script_folder(script)
+        if not self.script_folder:
+            raise BadConfiguration("Could not determine script folder.")
diff --git a/tools/suds_devel/egg.py b/tools/suds_devel/egg.py
new file mode 100644
index 0000000..93886af
--- /dev/null
+++ b/tools/suds_devel/egg.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Manipulating egg distributions.
+
+"""
+
+import os
+import os.path
+
+from suds_devel.zip import zip_folder_content
+
+
+def zip_eggs_in_folder(folder):
+    """
+    Make all egg distributions in the given folder be stored as egg files.
+
+    In case setuptools downloads one of its target packages as an unzipped egg
+    folder (e.g. if installed from an already installed unzipped egg), we need
+    to zip it ourselves. This is because there seems to be no way to make
+    setuptools perform an installation from a local unzipped egg folder.
+    Specifying either that folder or its parent folder as a setuptools
+    find-links URL just makes the folder be treated as a regular non-egg
+    folder.
+
+    """
+    eggs = _detect_eggs_in_folder(folder)
+    for egg in eggs:
+        egg.normalize()
+
+
+# Egg distribution related file & folder name extensions.
+_egg_ext = os.extsep + "egg"
+_zip_ext = _egg_ext + os.extsep + "zip"
+
+
+class _Egg:
+    """
+    Represents a single egg distribution.
+
+    Helps track & manage formats the distribution is stored in:
+      - zipped egg file with .egg extension
+      - zipped egg file with .egg.zip extension
+      - unzipped egg folder
+
+    """
+
+    # Indicators whether the egg distribution has a '.egg' file or folder.
+    NONE = object()
+    FILE = object()
+    FOLDER = object()
+
+    def __init__(self, path, egg, zip):
+        assert egg in (_Egg.NONE, _Egg.FILE, _Egg.FOLDER)
+        assert zip.__class__ is bool
+        assert zip or egg is not _Egg.NONE
+        self.__path = path
+        self.__egg = egg
+        self.__zip = zip
+
+    def has_egg_file(self):
+        return self.__egg is _Egg.FILE
+
+    def has_egg_folder(self):
+        return self.__egg is _Egg.FOLDER
+
+    def has_zip(self):
+        return self.__zip
+
+    def normalize(self):
+        """
+        Makes sure this egg distribution is stored only as an egg file.
+
+        The egg file will be created from another existing distribution format
+        if needed.
+
+        """
+        if self.has_egg_file():
+            if self.has_zip():
+                self.__remove_zip()
+        else:
+            if self.has_egg_folder():
+                if not self.has_zip():
+                    self.__zip_egg_folder()
+                self.__remove_egg_folder()
+            self.__rename_zip_to_egg()
+
+    def set_egg(self, egg):
+        assert egg in (_Egg.FILE, _Egg.FOLDER)
+        assert self.__egg is _Egg.NONE
+        self.__egg = egg
+
+    def set_zip(self):
+        assert not self.__zip
+        self.__zip = True
+
+    def __path_egg(self):
+        return self.__path + _egg_ext
+
+    def __path_zip(self):
+        return self.__path + _zip_ext
+
+    def __remove_egg_folder(self):
+        assert self.has_egg_folder()
+        import shutil
+        shutil.rmtree(self.__path_egg())
+        self.__egg = _Egg.NONE
+
+    def __remove_zip(self):
+        assert self.has_zip()
+        os.remove(self.__path_zip())
+        self.__zip = False
+
+    def __rename_zip_to_egg(self):
+        assert self.has_zip()
+        assert not self.has_egg_file()
+        assert not self.has_egg_folder()
+        os.rename(self.__path_zip(), self.__path_egg())
+        self.__egg = _Egg.FILE
+        self.__zip = False
+
+    def __zip_egg_folder(self):
+        assert self.has_egg_folder()
+        assert not self.has_zip()
+        zip_folder_content(self.__path_egg(), self.__path_zip())
+        self.__zip = True
+
+
+def _detect_eggs_in_folder(folder):
+    """
+    Detect egg distributions located in the given folder.
+
+    Only direct folder content is considered and subfolders are not searched
+    recursively.
+
+    """
+    eggs = {}
+    for x in os.listdir(folder):
+        zip = x.endswith(_zip_ext)
+        if zip:
+            root = x[:-len(_zip_ext)]
+            egg = _Egg.NONE
+        elif x.endswith(_egg_ext):
+            root = x[:-len(_egg_ext)]
+            if os.path.isdir(os.path.join(folder, x)):
+                egg = _Egg.FOLDER
+            else:
+                egg = _Egg.FILE
+        else:
+            continue
+        try:
+            info = eggs[root]
+        except KeyError:
+            eggs[root] = _Egg(os.path.join(folder, root), egg, zip)
+        else:
+            if egg is not _Egg.NONE:
+                info.set_egg(egg)
+            if zip:
+                info.set_zip()
+    return eggs.values()
diff --git a/tools/suds_devel/environment.py b/tools/suds_devel/environment.py
new file mode 100644
index 0000000..46f6d86
--- /dev/null
+++ b/tools/suds_devel/environment.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Class representing a single Python environment.
+
+Includes support for:
+  - fetching information about a specific Python environment
+  - executing a command in a specific Python environment
+
+"""
+
+import sys
+have_subprocess_devnull = sys.version_info >= (3, 3)
+if not have_subprocess_devnull:
+    import os
+import subprocess
+
+
+class BadEnvironment(Exception):
+    """
+    Problem occurred while scanning a Python environment.
+
+    The problem may be either a technical one in the scanning process itself,
+    or it may be a problem with something detected about the Python environment
+    in question, e.g. it might be using an incompatible Python version.
+
+    Specifying the environment scan result information when constructing this
+    exception is optional and may be added to an exception later on using the
+    set_raw_scan_results() method.
+
+    """
+
+    def __init__(self, message, out=None, err=None, exit_code=None):
+        Exception.__init__(self, message)
+        self.message = message
+        self.out = out
+        self.err = err
+        self.exit_code = exit_code
+
+    def raw_scan_results(self):
+        return self.out, self.err, self.exit_code
+
+    def set_raw_scan_results(self, out, err, exit_code):
+        assert self.out is None
+        assert self.err is None
+        assert self.exit_code is None
+        self.out = out
+        self.err = err
+        self.exit_code = exit_code
+
+
+class _UndefinedParameter:
+    """Internal class used to indicate undefined parameter values."""
+    pass
+
+
+class Environment:
+    """
+    Represents a single Python environment.
+
+    Allows running commands using the environment's Python interpreter.
+    Allows running an initial Python environment scan to collect information
+    about it.
+
+    Note that most of the information about the environment will not be
+    available until the initial environment scan is run.
+
+    """
+
+    def __init__(self, name, command):
+        self.__name = name
+        self.__command = command
+        self.initial_scan_completed = False
+
+    def command(self):
+        return self.__command
+
+    def description(self):
+        return "%s on %s" % (self.sys_version, self.sys_platform)
+
+    def execute(self, args=[], input=None, capture_output=False,
+            cwd=_UndefinedParameter):
+        stdin = subprocess.PIPE
+        close_stdin = False
+        if input is None:
+            if have_subprocess_devnull:
+                stdin = subprocess.DEVNULL
+            else:
+                stdin = open(os.devnull, "r")
+                close_stdin = True
+        try:
+            kwargs = {}
+            kwargs["stdin"] = stdin
+            if capture_output:
+                kwargs["stdout"] = subprocess.PIPE
+                kwargs["stderr"] = subprocess.PIPE
+            if cwd is not _UndefinedParameter:
+                kwargs["cwd"] = cwd
+            kwargs["universal_newlines"] = True
+            popen = subprocess.Popen([self.command()] + args, **kwargs)
+            out, err = popen.communicate(input)
+        finally:
+            if close_stdin:
+                stdin.close()
+        return out, err, popen.returncode
+
+    def name(self):
+        return self.__name
+
+    def run_initial_scan(self):
+        self.initial_scan_completed = False
+        scanner = self.__initial_scanner()
+        try:
+            scan_results = scanner.scan(self)
+            self.__collect_scanned_values(scan_results)
+            self.__parse_scanned_version_info()
+            self.initial_scan_completed = True
+            self.__check_python_version()
+        except BadEnvironment:
+            sys.exc_info()[1].set_raw_scan_results(*scanner.raw_scan_results())
+            raise
+        return scanner.raw_scan_results()
+
+    def __check_python_version(self):
+        if self.sys_version_info < (2, 4):
+            raise BadEnvironment("Unsupported Python version (%s, %s-bit)"
+                % (self.python_version, self.pointer_size_in_bits))
+
+    def __collect_scanned_values(self, values):
+        v = values
+        # System.
+        self.platform_architecture = v["platform.architecture"]
+        self.pointer_size_in_bits = v["pointer size in bits"]
+        self.sys_path = v["sys.path"]
+        self.sys_version = v["sys.version"]
+        self.sys_version_info_formatted = v["sys.version_info (formatted)"]
+        self.sys_version_info_raw = v["sys.version_info (raw)"]
+        self.sys_platform = v["sys.platform"]
+        self.sys_executable = v["sys.executable"]
+        # Packages.
+        self.ctypes_version = v["ctypes version"]
+        self.pip_version = v["pip version"]
+        self.pytest_version = v["pytest version"]
+        self.setuptools_version = v["setuptools version"]
+        self.virtualenv_version = v["virtualenv version"]
+        # Extra package info.
+        self.setuptools_zipped_egg = v["setuptools zipped egg"]
+
+    def __construct_python_version(self):
+        """
+        Construct a setuptools compatible Python version string.
+
+        Constructed based on the environment's reported sys.version_info.
+
+        """
+        major, minor, micro, release_level, serial = self.sys_version_info
+        assert release_level in ("alfa", "beta", "candidate", "final")
+        assert release_level != "final" or serial == 0
+        parts = [str(major), ".", str(minor), ".", str(micro)]
+        if release_level != "final":
+            parts.append(release_level[0])
+            parts.append(str(serial))
+        self.python_version = "".join(parts)
+
+    @staticmethod
+    def __initial_scanner():
+        s = EnvironmentScanner()
+
+        s.add_import("os.path")
+        s.add_import("platform")
+        s.add_import("struct")
+        s.add_import("sys")
+
+        s.add_function("""\
+def version_info_string():
+    major, minor, micro, release_level, serial = sys.version_info
+    if not (isinstance(major, int) and isinstance(minor, int) and
+        isinstance(micro, int) and isinstance(release_level, str) and
+        isinstance(serial, int)) or "," in release_level:
+        return ""
+    # At least Python versions 2.7.6 & 3.1.3 require an explicit tuple() cast.
+    return "%d,%d,%d,%s,%d" % tuple(sys.version_info)
+""")
+        s.add_function("""\
+def setuptools_zipped_egg():
+    try:
+        from pkg_resources import (
+            DistributionNotFound, EGG_DIST, get_distribution)
+    except ImportError:
+        return Skip
+    try:
+        d = get_distribution("setuptools")
+    except DistributionNotFound:
+        return Skip
+    if d.precedence != EGG_DIST or not os.path.isfile(d.location):
+        return Skip  # file = zipped egg; folder = unzipped egg
+    return d.location
+""")
+
+        s.add_field("platform.architecture", "platform.architecture()")
+        s.add_field("pointer size in bits", '8 * struct.calcsize("P")')
+        s.add_field("sys.executable")
+        s.add_field("sys.path")
+        s.add_field("sys.platform")
+        s.add_field("sys.version")
+        s.add_field("sys.version_info (formatted)", "version_info_string()")
+        s.add_field("sys.version_info (raw)", "sys.version_info")
+
+        s.add_package_version_field("ctypes", default=None)
+        s.add_package_version_field("pip", default=None)
+        s.add_package_version_field("pytest", default=None)
+        s.add_package_version_field("setuptools", default=None)
+        s.add_package_version_field("virtualenv", default=None)
+
+        s.add_field("setuptools zipped egg", "setuptools_zipped_egg()", None)
+        return s
+
+    def __parse_scanned_version_info(self):
+        """Parses the environment's formatted version info string."""
+        string = self.sys_version_info_formatted
+        try:
+            major, minor, micro, release_level, serial = string.split(",")
+            if (release_level in ("alfa", "beta", "candidate", "final") and
+                    (release_level != "final" or serial == "0") and
+                    major.isdigit() and  # --- --- --- --- --- --- --- --- ---
+                    minor.isdigit() and  # Explicit isdigit() checks to detect
+                    micro.isdigit() and  # leading/trailing whitespace.
+                    serial.isdigit()):   # --- --- --- --- --- --- --- --- ---
+                self.sys_version_info = (int(major), int(minor), int(micro),
+                    release_level, int(serial))
+                self.__construct_python_version()
+                return
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            pass
+        raise BadEnvironment("Unsupported Python version (%s)" % (string,))
+
+
+class EnvironmentScanner:
+    """
+    Allows scanning a given Python environment for specific information.
+
+    Runs a scanner script in the given Python environment, crafted based on the
+    information we wish to find out about the environment, and then collects
+    and returns the requested information.
+
+    """
+
+    # Explicitly specified scan output start & finish markers allow us to
+    # safely ignore extra output that might be added by the environment's
+    # Python interpreter startup. This can be useful, for instance, if you want
+    # to debug this script by having the Python interpreter startup script
+    # output the exact calls made to the Python interpreter.
+    SCAN_START_MARKER = "--- SCAN START ---"
+    SCAN_FINISH_MARKER = "--- SCAN FINISH ---"
+
+    __scanner_script__startup = """\
+class Skip:
+    pass
+
+def print_field(id, value):
+    if value is not Skip:
+        print("%s: %s" % (id, value))
+"""
+
+    # If available, setuptools can give us more detailed version information
+    # read from the package's meta-data than simply reading it from its
+    # __version__ attribute. For example, in case of setuptools it can tell us
+    # that we are dealing with version '3.7dev' while __version__ would tell us
+    # just '3.7'. Also, some older packages, such as pip 1.1, may not have a
+    # __version__ attribute set in their main module at all.
+    __scanner_script__package_version_scanner = """\
+def package_version(package_name):
+    try:
+        package = __import__(package_name, {}, {}, ("__version__",))
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        return Skip  # No package - no version.
+    try:
+        version = package.__version__
+    except AttributeError:
+        version = "unknown"
+    try:
+        from pkg_resources import (DistributionNotFound, get_distribution)
+    except ImportError:
+        return version
+    try:
+        return get_distribution(package_name).version
+    except DistributionNotFound:
+        return version
+"""
+
+    def __init__(self):
+        self.__out = None
+        self.__err = None
+        self.__exit_code = None
+        self.__scan_for_version_info = False
+        self.__extra_script_functions = []
+        self.__extra_script_imports = []
+        self.__fields = []
+
+    def add_field(self, id, getter=None, default=_UndefinedParameter):
+        if getter is None:
+            getter = id
+        self.__add_field(id, "print_field(%r, %s)" % (id, getter), default)
+
+    def add_function(self, code):
+        self.__extra_script_functions.append(code)
+
+    def add_import(self, module_name):
+        self.__extra_script_imports.append(module_name)
+
+    def add_package_version_field(self, package_name, default=_UndefinedParameter):
+        self.__scan_for_version_info = True
+        field_id = "%s version" % (package_name,)
+        field_getter = "print_field(%r, package_version(%r))" % (
+            field_id, package_name)
+        self.__add_field(field_id, field_getter, default)
+
+    def raw_scan_results(self):
+        return self.__out, self.__err, self.__exit_code
+
+    def scan(self, environment):
+        self.__scan(environment)
+        raw_data = self.__parse_raw_scanner_output()
+        return self.__map_raw_data_to_expected_fields(raw_data)
+
+    def __add_field(self, id, getter, default):
+        assert id not in [x[0] for x in self.__fields]
+        self.__fields.append((id, getter, default))
+
+    @staticmethod
+    def __extract_value(raw_data, id, default):
+        if default is not _UndefinedParameter:
+            return raw_data.pop(id, default)
+        try:
+            return raw_data.pop(id)
+        except KeyError:
+            raise BadEnvironment("Missing scan output record (%s)" % (
+                sys.exc_info()[1],))
+
+    def __map_raw_data_to_expected_fields(self, raw_data):
+        scanner_results = {}
+        for id, getter, default in self.__fields:
+            assert id not in scanner_results
+            scanner_results[id] = self.__extract_value(raw_data, id, default)
+        if raw_data:
+            raise BadEnvironment("Extra scan output records (%s)" % (
+                ",".join(raw_data.keys()),))
+        return scanner_results
+
+    def __parse_raw_scanner_output(self):
+        assert self.__scanned()
+        result = {}
+        in_scanner_output = False
+        for line in self.__out.split("\n"):
+            if not in_scanner_output:
+                in_scanner_output = line.startswith(self.SCAN_START_MARKER)
+                continue
+            if line.startswith(self.SCAN_FINISH_MARKER):
+                return result
+            split_result = line.split(": ", 1)
+            if len(split_result) != 2:
+                raise BadEnvironment("Error parsing scan output record")
+            key, value = split_result
+            if key in result:
+                raise BadEnvironment("Duplicate scan output record (%s)" %
+                    (key,))
+            result[key] = value
+        if not in_scanner_output:
+            raise BadEnvironment("No valid scan output detected")
+        raise BadEnvironment("Scan output truncated")
+
+    def __scan(self, environment):
+        assert not self.__scanned()
+        try:
+            self.__out, self.__err, self.__exit_code = environment.execute(
+                input=self.__scanner_script(), capture_output=True)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            e_type, e = sys.exc_info()[:2]
+            try:
+                raise BadEnvironment("%s: %s" % (e_type.__name__, e))
+            finally:
+                del e  # explicitly break circular reference chain in Python 3
+        if self.__exit_code != 0:
+            raise BadEnvironment("Scan failed (exit code %d)" %
+                (self.__exit_code,))
+        if self.__err:
+            raise BadEnvironment("Scan failed (error output detected)")
+        if not self.__out:
+            raise BadEnvironment("Scan failed (no output)")
+
+    def __scanned(self):
+        return self.__exit_code is not None
+
+    def __scanner_script(self):
+        script_lines = []
+        if self.__extra_script_imports:
+            for x in self.__extra_script_imports:
+                script_lines.append("import %s" % (x,))
+            script_lines.append("")
+        script_lines.append(self.__scanner_script__startup)
+        script_lines.append("")
+        if self.__scan_for_version_info:
+            script_lines.append(self.__scanner_script__package_version_scanner)
+        script_lines.extend(self.__extra_script_functions)
+        script_lines.append("")
+        script_lines.append('print("%s")' % (self.SCAN_START_MARKER,))
+        script_lines.extend(getter for id, getter, default in self.__fields)
+        script_lines.append('print("%s")' % (self.SCAN_FINISH_MARKER,))
+        script_lines.append("")
+        return "\n".join(script_lines)
diff --git a/tests/conftest.py b/tools/suds_devel/exception.py
similarity index 55%
copy from tests/conftest.py
copy to tools/suds_devel/exception.py
index c706614..116eefe 100644
--- a/tests/conftest.py
+++ b/tools/suds_devel/exception.py
@@ -16,16 +16,17 @@
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
-pytest configuration file for the suds test suite.
+Generic exception classes shared in different development utility modules.
 
 """
 
-# Make pytest load custom plugins expected to be loaded in our test suite.
-#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
-# information in its --markers list if called from a folder other than the one
-# containing the tests folder or if the tests folder is not on the current
-# Python path, e.g. if using pytest in the Python 3 implementation 'build'
-# folder constructed by 'setup.py build' using 'py.test build --markers'. The
-# plugin will still get loaded correctly when actually running the tests. This
-# has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+class EnvironmentSetupError(Exception):
+    """
+    Signal to move onto setting up the next environment.
+
+    Raised after the current environment's setup fails. Error message stored in
+    the exception is expected to be reported to the user.
+
+    """
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/tests/conftest.py b/tools/suds_devel/ez_setup_versioned.py
similarity index 55%
copy from tests/conftest.py
copy to tools/suds_devel/ez_setup_versioned.py
index c706614..f8bd58e 100644
--- a/tests/conftest.py
+++ b/tools/suds_devel/ez_setup_versioned.py
@@ -16,16 +16,24 @@
 # written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
 
 """
-pytest configuration file for the suds test suite.
+Managing Python version specific setuptools ez_setup setup modules used in this
+project.
 
 """
 
-# Make pytest load custom plugins expected to be loaded in our test suite.
-#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
-# information in its --markers list if called from a folder other than the one
-# containing the tests folder or if the tests folder is not on the current
-# Python path, e.g. if using pytest in the Python 3 implementation 'build'
-# folder constructed by 'setup.py build' using 'py.test build --markers'. The
-# plugin will still get loaded correctly when actually running the tests. This
-# has already been reported as a pytest issue.
-pytest_plugins = "tests.indirect_parametrize"
+import sys
+
+
+def import_module(version_info=sys.version_info):
+    return __import__(module_name(version_info))
+
+
+def script_name(version_info=sys.version_info):
+    return module_name(version_info) + ".py"
+
+
+def module_name(version_info=sys.version_info):
+    if version_info < (2, 6):
+        # setuptools 1.4.2 - the final supported release on Python 2.4 & 2.5.
+        return "ez_setup_1_4_2"
+    return "ez_setup"
diff --git a/tools/suds_devel/parse_version.py b/tools/suds_devel/parse_version.py
new file mode 100644
index 0000000..fd89f28
--- /dev/null
+++ b/tools/suds_devel/parse_version.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Utility for converting version strings into a directly comparable tuple.
+
+Shamelessly stolen from setuptools 3.7 (development version) where this code is
+located in its pkg_resources.py module. The code has not been modified in any
+way, including comments and coding style, so that it would be as easy as
+possible to upgrade it to a newer version if that becomes needed.
+
+Having this functionality in our project allows our tool scripts to run even
+without locally installed setuptools installed, e.g. the script for setting up
+the base development environments.
+
+"""
+
+import re
+
+component_re = re.compile(r'(\d+ | [a-z]+ | \.| -)', re.VERBOSE)
+replace = {'pre':'c', 'preview':'c','-':'final-','rc':'c','dev':'@'}.get
+
+def _parse_version_parts(s):
+    for part in component_re.split(s):
+        part = replace(part, part)
+        if not part or part=='.':
+            continue
+        if part[:1] in '0123456789':
+            # pad for numeric comparison
+            yield part.zfill(8)
+        else:
+            yield '*'+part
+
+    # ensure that alpha/beta/candidate are before final
+    yield '*final'
+
+def parse_version(s):
+    """Convert a version string to a chronologically-sortable key
+
+    This is a rough cross between distutils' StrictVersion and LooseVersion;
+    if you give it versions that would work with StrictVersion, then it behaves
+    the same; otherwise it acts like a slightly-smarter LooseVersion. It is
+    *possible* to create pathological version coding schemes that will fool
+    this parser, but they should be very rare in practice.
+
+    The returned value will be a tuple of strings.  Numeric portions of the
+    version are padded to 8 digits so they will compare numerically, but
+    without relying on how numbers compare relative to strings.  Dots are
+    dropped, but dashes are retained.  Trailing zeros between alpha segments
+    or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
+    "2.4". Alphanumeric parts are lower-cased.
+
+    The algorithm assumes that strings like "-" and any alpha string that
+    alphabetically follows "final"  represents a "patch level".  So, "2.4-1"
+    is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
+    considered newer than "2.4-1", which in turn is newer than "2.4".
+
+    Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
+    come before "final" alphabetically) are assumed to be pre-release versions,
+    so that the version "2.4" is considered newer than "2.4a1".
+
+    Finally, to handle miscellaneous cases, the strings "pre", "preview", and
+    "rc" are treated as if they were "c", i.e. as though they were release
+    candidates, and therefore are not as new as a version string that does not
+    contain them, and "dev" is replaced with an '@' so that it sorts lower than
+    than any other pre-release tag.
+    """
+    parts = []
+    for part in _parse_version_parts(s.lower()):
+        if part.startswith('*'):
+            # remove '-' before a prerelease tag
+            if part<'*final':
+                while parts and parts[-1]=='*final-': parts.pop()
+            # remove trailing zeros from each series of numeric parts
+            while parts and parts[-1]=='00000000':
+                parts.pop()
+        parts.append(part)
+    return tuple(parts)
diff --git a/tools/suds_devel/patch_pytest_on_python_31.py b/tools/suds_devel/patch_pytest_on_python_31.py
new file mode 100644
index 0000000..db381a3
--- /dev/null
+++ b/tools/suds_devel/patch_pytest_on_python_31.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Support for patching an existing pytest installation to make it work correctly
+in a Python 3.1 environment.
+
+pytest versions prior to its 2.6 release do not support Python 3.1 out of the
+box, but can be patched to do so. Basic gist of this patch is to replace a
+single call to the builtin function 'callable()' in pytest's _pytest/runner.py
+module with a call to 'py.builtin.callable()'. The patch has been tested to
+make pytest 2.5.2 work correctly with Python 3.1 and is based on code found in
+pytest pull request #168 & commit 04c4997da865344f2ebb8569c73c51c57cd4ba05.
+
+"""
+
+import os
+import os.path
+import re
+import sys
+
+from suds_devel.environment import EnvironmentScanner
+from suds_devel.exception import EnvironmentSetupError
+from suds_devel.parse_version import parse_version
+
+
+def patch(env):
+    assert env.sys_version_info[:2] == (3, 1)
+    pytest_location, pytest_version = _scan(env)
+    if parse_version(pytest_version) >= parse_version("2.6.0"):
+        return
+    print("Patching installed pytest package...")
+    file_path = os.path.join(pytest_location, "_pytest", "runner.py")
+    file_temp_path = _file_temp_path(file_path)
+    try:
+        try:
+            prepatched_count, patched_count = _patch(file_path, file_temp_path,
+                unpatched_regex="(.*\s)callable([(].*)$",
+                patched_regex="(.*\s)py[.]builtin[.]callable([(].*)",
+                patch_pattern="%spy.builtin.callable%s")
+            if (prepatched_count, patched_count) not in ((1, 0), (0, 1)):
+                _error(file_path, "file content not recognized")
+            if prepatched_count:
+                print("WARNING: pytest already patched")
+            else:
+                os.remove(file_path)
+                os.rename(file_temp_path, file_path)
+        finally:
+            _try_remove_file(file_temp_path)
+    except (EnvironmentSetupError, KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        _error(file_path, str(sys.exc_info()[1]))
+
+
+def _error(file_path, message):
+    raise EnvironmentSetupError("can not patch pytest module '%s' - %s" % (
+        file_path, message))
+
+
+def _file_temp_path(file_path):
+    for i in range(100):
+        temp_path = "%s.patching.%d.tmp" % (file_path, i)
+        if not os.path.exists(temp_path):
+            return temp_path
+    _error(file_path, "can not find available temp file name")
+
+
+def _patch(source, dest, unpatched_regex, patched_regex, patch_pattern):
+    """
+    Patch given source file into the given destination file.
+
+    Patching is done line by line. Given un-patched and patched line detection
+    regular expressions are expected never to match the same line.
+
+    Does not modify the source file in any way.
+
+    Returns the number of detected pre-patched lines, and a number of newly
+    patched lines.
+
+    """
+    prepatched_count = 0
+    patched_count = 0
+    unpatched_matcher = re.compile(unpatched_regex, re.DOTALL)
+    patched_matcher = re.compile(patched_regex)
+    f_in = None
+    f_out = None
+    try:
+        f_in = open(source, "r")
+        f_out = open(dest, mode="w")
+        for line in f_in:
+            match = unpatched_matcher.match(line)
+            if match:
+                assert not patched_matcher.match(line)
+                patched_count += 1
+                line = patch_pattern % match.groups()
+                assert patched_matcher.match(line)
+            elif patched_matcher.match(line):
+                prepatched_count += 1
+            f_out.write(line)
+    finally:
+        if f_in:
+            f_in.close()
+        if f_out:
+            f_out.close()
+    return prepatched_count, patched_count
+
+
+def _scan(env):
+    """Scan the given Python environment's pytest installation."""
+    s = EnvironmentScanner()
+    s.add_function("""\
+def get_pytest_location():
+    from pkg_resources import get_distribution
+    return get_distribution("pytest").location
+""")
+    s.add_field("pytest location", "get_pytest_location()")
+    s.add_package_version_field("pytest", default="")
+    scan_results = s.scan(env)
+    return scan_results["pytest location"], scan_results["pytest version"]
+
+
+def _try_remove_file(path):
+    try:
+        os.remove(path)
+    except (KeyboardInterrupt, SystemExit):
+        raise
+    except Exception:
+        pass
diff --git a/tools/suds_devel/requirements.py b/tools/suds_devel/requirements.py
new file mode 100644
index 0000000..ab04fc3
--- /dev/null
+++ b/tools/suds_devel/requirements.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Package requirements for this project.
+
+Extracted here so they can be reused between the project's setup and different
+development utility scripts.
+
+Python 2.4 pytest compatibility notes:
+--------------------------------------
+
+pytest versions prior to 2.4.0 may be installed but will fail at runtime when
+running our test suite, as they can not parse all of the pytest constructs used
+in this project, e.g. skipif expressions not given as strings. Versions 2.4.2
+and later can not be installed at all.
+
+pytest 2.4.0 release formally broke compatibility with Python releases prior to
+2.5 and the last officially supported pytest version on Python 2.4 platforms is
+2.3.5.
+
+Our tests can still be run using a Python 2.4.x environment if the following
+package versions are installed into it:
+ - pytest - not older than 2.4.0 nor equal to or newer than 2.4.2
+ - py - older than 1.4.16 (version 1.4.16 may be installed but will cause
+   pytest to fail when running our test suite).
+
+Listed package versions can be installed together using a pip command like:
+  install pytest>=2.4.0,<2.4.2 py<1.4.16
+
+Listed pytest versions specify py version 1.4.16+ as their requirement but work
+well enough for us with this older py release. Note though that due to pytest
+not having its requirements formally satisfied, some operations related to it
+may fail unexpectedly. For example, running setuptools installed 'py.test'
+startup scripts will fail, as they explicitly check that all the formally
+specified pytest requirements have been met, but pytest can still be started
+using 'py24 -m pytest'.
+
+See the project's Python compatibility related hacking docs for more detailed
+information.
+
+"""
+
+import sys
+
+from suds_devel.parse_version import parse_version
+from suds_devel.utility import (lowest_version_string_with_prefix,
+    requirement_spec)
+
+
+class _Unspecified:
+    pass
+
+
+_first_unsupported_py_version_on_Python_24 = (
+    lowest_version_string_with_prefix("1.4.16"))
+_first_supported_pytest_version_on_Python_24 = "2.4.0"
+_first_unsupported_pytest_version_on_Python_24 = (
+    lowest_version_string_with_prefix("2.4.2"))
+
+
+def check_Python24_pytest_requirements():
+    """
+    Check pytest requirements in the current Python 2.4.x environment.
+
+    Installing pytest into a Python 2.4.x environment requires specific py &
+    pytest package versions. This function checks whether the environment has
+    such compatible Python environments installed.
+
+    Returns a 2-tuple (have_pytest, have_py) indicating whether valid pytest &
+    py library packages have been detected in the current Python 2.4.x
+    environment. If the pytest package has not been detected, the py library
+    package will not be checked and the have_py value will be set to None.
+
+    See the module docstring for more detailed information.
+
+    """
+    assert sys.version_info[:2] == (2, 4)
+    try:
+        from pytest import __version__ as pytest_version
+    except ImportError:
+        return False, None  # no pytest
+    pv_from = parse_version(_first_supported_pytest_version_on_Python_24)
+    pv_to = parse_version(_first_unsupported_pytest_version_on_Python_24)
+    if not (pv_from <= parse_version(pytest_version) < pv_to):
+        return False, None  # incompatible pytest version
+    try:
+        from py import __version__ as py_version
+    except ImportError:
+        return True, False  # no py library package
+    pv_unsupported = parse_version(_first_unsupported_py_version_on_Python_24)
+    if parse_version(py_version) >= pv_unsupported:
+        return True, False  # incompatible py library package version
+    return True, True
+
+
+def pytest_requirements(version_info=None, ctypes_version=_Unspecified):
+    """
+    Generate Python version specific pytest package requirements.
+
+    The requirements are returned as setuptools/pip compatible requirement
+    specification strings.
+
+    As a slight optimization, specify no Python version information to indicate
+    that the requirements are being listed for the current Python environment.
+
+    Missing ctypes installation should be indicated by setting ctypes_version
+    parameter to None, while not specifying it indicates that no ctypes version
+    information is provided.
+
+    """
+    current_environment = version_info is None
+    if current_environment:
+        version_info = sys.version_info
+
+    pytest_version = None
+    if version_info < (2, 5):
+        pytest_version = (
+            (">=", _first_supported_pytest_version_on_Python_24),
+            ("<", _first_unsupported_pytest_version_on_Python_24))
+        yield requirement_spec("py",
+            ("<", _first_unsupported_py_version_on_Python_24))
+        #IDEA: In this case we could run the pytest installation separately
+        # from all the other pip based installations and have it not install
+        # pytest scripts. Since in general there is no 'setup.py install' or
+        # 'pip' command-line argument that can say 'do not install scripts',
+        # this would most likely need to use a pip command-line option like
+        # '--install-option="--install-scripts=..."' to make the scripts be
+        # installed into a temporary folder and then remove that folder after
+        # the installation. An alternative would be to use easy_install which
+        # does support the --exclude-scripts command-line option. N.B. This
+        # could work for the project's Python environment setup scripts but not
+        # when installing the the project's using its setup script as that
+        # script expects to set up all the required packages itself.
+
+    # pytest on Windows depends on the colorama package, and that package has
+    # several accidental backward compatibility issues we have to work around
+    # when using Python 2.5.
+    elif version_info < (2, 6):
+        if sys.platform == "win32":
+            # colorama releases [0.1.11 - 0.3.2> do not work unless the ctypes
+            # module is available, but that module is not included in 64-bit
+            # CPython distributions (tested using Python 2.5.4). Some of those
+            # versions fail to install, while others only fail at run-time.
+            # Pull request https://github.com/tartley/colorama/pull/4 resolves
+            # this issue for colorama release 0.3.2.
+            if ctypes_version is _Unspecified:
+                assert current_environment
+                try:
+                    from ctypes import __version__ as ctypes_version
+                except ImportError:
+                    ctypes_version = None
+            if ctypes_version is None:
+                # We could try to install an external 'ctypes' package from
+                # PyPI here, but that would require an old C++ compiler and so
+                # would not be highly likely to work in any concurrent
+                # development environment.
+                #
+                #TODO: When using colorama releases older than 0.1.11, you
+                # might get atexit() errors on process shutdown after running
+                # the project's 'setup.py test' command and having it
+                # automatically install the colorama package in the process.
+                # The error itself is benign and there are no other effects to
+                # it other than the error message getting displayed. The whole
+                # issue is caused by colorama's atexit handler getting called
+                # multiple times due to some internal setuptools module loading
+                # and unloading. We found no easy workaround to this so update
+                # this code to use the colorama package version 0.3.2+ as soon
+                # as it gets released. When this is done, also remove a related
+                # setup.py comment.
+                v_bad_low = lowest_version_string_with_prefix("0.1.11")
+                v_bad_high = "0.3.2"
+                version_spec = ("<", v_bad_low), (">=", v_bad_high)
+            else:
+                # colorama 0.3.1 release accidentally uses the 'with' keyword
+                # without a corresponding __future__ import in its setup.py
+                # script.
+                version_spec = ("!=", "0.3.1"),
+            yield requirement_spec("colorama", *version_spec)
+
+    # Python 3.0 & 3.1 stdlib does not include the argparse module which pytest
+    # requires, even though it does not list it explicitly among its
+    # requirements. Python 3.x series introduced the argparse module in its 3.2
+    # release so it needs to be installed manually in 3.0.x & 3.1.x releases.
+    # Tested that pytest 2.5.2+ requires py 1.4.20+ which in turn requires the
+    # argparse module but does not specify this dependency explicitly.
+    elif (3,) <= version_info < (3, 2):
+        # pytest versions prior to 2.6.0 are not compatible with Python 3.1,
+        # mostly due to accidental incompatibilities introduced because that is
+        # not one of the officially supported platforms for pytest and so does
+        # not get regular testing. Development version 2.6.0 has been tested
+        # with this project and found to work correctly.
+        #TODO: Once pytest 2.6.0 has been officially released change the pytest
+        # requirement to ("==", "2.6.0").
+        pytest_version = (">=", lowest_version_string_with_prefix("2.6.0")),
+        missing_argparse = True
+        if current_environment:
+            try:
+                import argparse
+                missing_argparse = False
+            except ImportError:
+                pass
+        if missing_argparse:
+            yield requirement_spec("argparse")
+
+    if not pytest_version:
+        # pytest versions prior to 2.4.0 do not support non-string 'skipif'
+        # expressions.
+        pytest_version = (">=", "2.4.0"),
+    yield requirement_spec("pytest", *pytest_version)
+
+
+def six_requirements(version_info=sys.version_info):
+    """
+    Generate Python version specific six package requirements.
+
+    The requirements are returned as setuptools/pip compatible requirement
+    specification strings.
+
+    """
+    if version_info < (2, 5):
+        # 'six' release 1.5 dropped Python 2.4.x compatibility.
+        yield requirement_spec("six",
+            ("<", lowest_version_string_with_prefix("1.5")))
+    else:
+        yield requirement_spec("six")
+
+
+def virtualenv_requirements(version_info=sys.version_info):
+    """
+    Generate Python version specific virtualenv package requirements.
+
+    The requirements are returned as setuptools/pip compatible requirement
+    specification strings.
+
+    """
+    if version_info < (2, 5):
+        # 'virtualenv' release 1.8 dropped Python 2.4.x compatibility.
+        yield requirement_spec("virtualenv",
+            ("<", lowest_version_string_with_prefix("1.8")))
+    elif version_info < (2, 6):
+        # 'virtualenv' release 1.10 dropped Python 2.5.x compatibility.
+        yield requirement_spec("virtualenv",
+            ("<", lowest_version_string_with_prefix("1.10")))
+    else:
+        yield requirement_spec("virtualenv")
diff --git a/tools/suds_devel/utility.py b/tools/suds_devel/utility.py
new file mode 100644
index 0000000..6a0761a
--- /dev/null
+++ b/tools/suds_devel/utility.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Generic functionality shared in different development utility modules.
+
+"""
+
+import os
+import os.path
+import sys
+if sys.version_info < (3, 0):
+    from urllib import quote as url_quote
+else:
+    from urllib.parse import quote as url_quote
+
+
+def any_contains_any(strings, candidates):
+    """Whether any of the strings contains any of the candidates."""
+    for string in strings:
+        for c in candidates:
+            if c in string:
+                return True
+
+
+class FileJanitor:
+    """Janitor class for removing a specific file."""
+
+    def __init__(self, path):
+        self.__path = path
+
+    def clean(self):
+        try:
+            os.remove(self.__path)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            pass
+
+
+def lowest_version_string_with_prefix(prefix):
+    """
+    The lowest possible version string with the given prefix.
+
+    'The lowest' according to the usual version string ordering used by
+    setuptools, e.g. '2.4.3.dev0' is the lowest possible version in the 2.4.3
+    series.
+
+    """
+    return "%s.dev0" % (prefix,)
+
+
+def path_to_URL(path, escape=True):
+    """Convert a local file path to a absolute path file protocol URL."""
+    # We do not use urllib's builtin pathname2url() function since:
+    #  - it has been commented with 'not recommended for general use'
+    #  - it does not seem to work the same on Windows and non-Windows platforms
+    #    (result starts with /// on Windows but does not on others)
+    #  - urllib implementation prior to Python 2.5 used to quote ':' characters
+    #    as '|' which would confuse pip on Windows.
+    url = os.path.abspath(path)
+    for sep in (os.sep, os.altsep):
+        if sep and sep != "/":
+            url = url.replace(sep, "/")
+    if escape:
+        # Must not escape ':' or '/' or Python will not recognize those URLs
+        # correctly. Detected on Windows 7 SP1 x64 with Python 3.4.0, but doing
+        # this always does not hurt since both are valid ASCII characters.
+        no_protocol_URL = url_quote(url, safe=":/")
+    else:
+        no_protocol_URL = url
+    return "file:///%s" % (no_protocol_URL,)
+
+
+def report_error(message):
+    print("ERROR: %s" % (message,))
+
+
+def requirement_spec(package_name, *args):
+    """Identifier used when specifying a requirement to pip or setuptools."""
+    if not args or args == (None,):
+        return package_name
+    version_specs = []
+    for version_spec in args:
+        if isinstance(version_spec, (list, tuple)):
+            operator, version = version_spec
+        else:
+            assert isinstance(version_spec, str)
+            operator = "=="
+            version = version_spec
+        version_specs.append("%s%s" % (operator, version))
+    return "%s%s" % (package_name, ",".join(version_specs))
+
+
+if sys.version_info < (2, 6):
+    def _rel_path(path):
+        # Older Python versions do not support os.path.relpath(), ergo no
+        # pretty path formatting for them.
+        return os.path.normpath(path)
+else:
+    def _rel_path(path):
+        try:
+            return os.path.relpath(path)
+        except ValueError:
+            # Current and given path on different file systems, e.g. C: & D:
+            # drives on Windows.
+            return os.path.normpath(path)
+
+
+def script_folder(script_path):
+    """
+    Return the given script's folder or None if it can not be determined.
+
+    Script is identified by its __file__ attribute. If the given __file__
+    attribute value contains no path information, it is expected to identify an
+    existing file in the current working folder.
+
+    Returned folder may be specified relative to the current working folder
+    and, if determined, will never be an empty string.
+
+    Typical use case for calling this function is from a regular stand-alone
+    script and not a frozen module or a module imported from the disk, a
+    zip-file, an external database or any other such source. Such callers can
+    safely assume they have a valid __file__ attribute available.
+
+    """
+    # There exist modules whose __file__ attribute does not correspond directly
+    # to a disk file, e.g. modules imported from inside zip archives.
+    if os.path.isfile(script_path):
+        return _rel_path(os.path.dirname(script_path)) or "."
+
+
+def path_iter(path):
+    """Returns an iterator over all the file & folder names in a path."""
+    parts = []
+    while path:
+        path, item = os.path.split(path)
+        if item:
+            parts.append(item)
+    return reversed(parts)
diff --git a/tools/suds_devel/zip.py b/tools/suds_devel/zip.py
new file mode 100644
index 0000000..783227a
--- /dev/null
+++ b/tools/suds_devel/zip.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Zip compression related utilities.
+
+"""
+
+import os
+import os.path
+import sys
+import zipfile
+
+from suds_devel.utility import path_iter
+
+
+def zip_folder_content(folder, zip_file):
+    success = False
+    zippy = zipfile.ZipFile(zip_file, "w", _zip_compression())
+    try:
+        archiver = _Archiver(zippy, folder)
+        for root, folders, files in os.walk(folder):
+            archiver.add_folder_with_files(root, files)
+        success = True
+    finally:
+        zippy.close()
+        if not success:
+            os.remove(zip_file)
+
+
+class _Archiver:
+
+    def __init__(self, zip_file, folder):
+        self.__zip_file = zip_file
+        self.__base_folder_parts = list(path_iter(folder))
+
+    def add_folder_with_files(self, folder, files):
+        path_prefix = self.__path_prefix(folder)
+        for file in files:
+            assert file
+            file_path = os.path.join(folder, file)
+            self.__zip_file.write(file_path, path_prefix + file)
+        # If no files are present in this folder and this is not the base
+        # folder then we need to add the folder itself as an explicit entry.
+        if not files and path_prefix:
+            # Old Python versions did not support using the ZipFile.write()
+            # method on folders so we do it manually by adding a 0-size entry
+            # using ZipFile.writestr(). Encountered using Python 2.4.3.
+            # N.B. An archived folder name must include a trailing slash, which
+            # is exactly what we have in our prepared path_prefix value.
+            self.__zip_file.writestr(path_prefix, "")
+
+    def __path_prefix(self, folder):
+        """
+        Path prefix to be used when archiving any items from the given folder.
+
+        Expects the folder to be located under the base folder path and the
+        returned path prefix does not include the base folder information. This
+        makes sure we include just the base folder's content in the archive,
+        and not the base folder itself.
+
+        """
+        path_parts = path_iter(folder)
+        _skip_expected(path_parts, self.__base_folder_parts)
+        result = "/".join(path_parts)
+        if result:
+            result += "/"
+        return result
+
+
+# Mimic the next() built-in introduced in Python 2.6. Does not need to be
+# perfect but only as good as we need it internally in this module.
+if sys.version_info < (2, 6):
+    def _iter_next(iter):
+        return iter.next()
+else:
+    _iter_next = next
+
+
+def _skip_expected(iter, expected):
+    for expected_value in expected:
+        value = _iter_next(iter)
+        assert value == expected_value
+
+
+_zip_compression_value = None
+
+def _zip_compression():
+    global _zip_compression_value
+    if _zip_compression_value is None:
+        try:
+            import zlib
+            _zip_compression_value = zipfile.ZIP_DEFLATED
+        except ImportError:
+            _zip_compression_value = zipfile.ZIP_STORED
+    return _zip_compression_value
commit f1d2f98179529913e31ac391a0f4aeed5eae659b
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:50:35 2014 +0200

    Releasing debian version 0.6-1.

diff --git a/debian/changelog b/debian/changelog
index fa49023..6c6396e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+suds (0.6-1) unstable; urgency=medium
+
+  * Adding upstream version 0.6.
+  * Adding Build-Depends.
+  * Remove tests.
+  * Remove tests from diocs.
+
+ -- Mathias Behrle <mathiasb at m9s.biz>  Mon, 30 Jun 2014 18:49:50 +0200
+
 suds (0.4.1-14) unstable; urgency=medium
 
   * Removing accidently doubled option pgpsigurlmangle from watch file.
commit 456b8df3b94a6630c16134c390e081f70ba314d1
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:49:32 2014 +0200

    Remove tests from diocs.

diff --git a/debian/python-suds.docs b/debian/python-suds.docs
index cdffa9e..a1320b1 100644
--- a/debian/python-suds.docs
+++ b/debian/python-suds.docs
@@ -1,2 +1 @@
-README
-tests
+README.rst
commit 9b808d08c40c81549949c59444daa78a8551f90d
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:48:19 2014 +0200

    Remove tests.

diff --git a/debian/patches/01-remove-makefile b/debian/patches/01-remove-makefile
deleted file mode 100644
index 0ae63b8..0000000
--- a/debian/patches/01-remove-makefile
+++ /dev/null
@@ -1,78 +0,0 @@
-Author: Mathias Behrle <mathiasb at m9s.biz>
-Description: Remove makefile to build correctly with dh_python2 and setup.py.
-
-diff -Naurp suds.orig/makefile suds/makefile
---- suds.orig/makefile	2012-06-30 16:44:34.670425102 +0200
-+++ suds/makefile	1970-01-01 01:00:00.000000000 +0100
-@@ -1,71 +0,0 @@
--# This program is free software; you can redistribute it and/or modify
--# it under the terms of the (LGPL) GNU Lesser General Public License as
--# published by the Free Software Foundation; either version 3 of the 
--# License, or (at your option) any later version.
--#
--# This program is distributed in the hope that it will be useful,
--# but WITHOUT ANY WARRANTY; without even the implied warranty of
--# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--# GNU Library Lesser General Public License for more details at
--# ( http://www.gnu.org/licenses/lgpl.html ).
--#
--# You should have received a copy of the GNU Lesser General Public License
--# along with this program; if not, write to the Free Software
--# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--# written by: Jeff Ortel ( jortel at redhat.com )
--#
--
--PKG = python-suds
--SPEC = $(PKG).spec
--SETUP = setup.py
--DOCTAR = suds-docs.tar.gz
--FEDORAPEOPLE = jortel at fedorapeople.org
--
--all : rpm docs
--
--dist : clean
--	mkdir dist
--	./sdist
--	./sdist python
--
--rpm : dist
--	cp dist/$(PKG)*.gz /usr/src/redhat/SOURCES
--	rpmbuild -ba $(SPEC)
--	cp /usr/src/redhat/RPMS/noarch/$(PKG)*.rpm dist
--	cp /usr/src/redhat/SRPMS/$(PKG)*.rpm dist
--	rpmlint -i dist/$(PKG)*.rpm
--
--release : rpm rdocs
--	scp dist/python*.tar.gz fedorahosted.org:suds
--	scp dist/python*.rpm fedorahosted.org:suds
--
--register :
--	python setup.py sdist bdist_egg register upload
--
--rdocs : docs
--	scp /tmp/$(DOCTAR) $(FEDORAPEOPLE):
--	ssh $(FEDORAPEOPLE) 'cd public_html/suds; rm -rf doc; tar xmzvf ~/$(DOCTAR)'
--
--docs :
--	rm -rf doc
--	rm -f /tmp/$(DOCTAR)
--	epydoc -vo doc `find suds -name "*.py"`
--	tar czvf /tmp/$(DOCTAR) doc
--
--pdf :
--	epydoc -vo doc --pdf `find suds -name \*.py`
--	mv doc/api.pdf doc/sudsapi.pdf
--
--clean :
--	rm -rf dist
--	rm -rf build
--	rm -rf doc
--	rm -rf *.egg-info
--	rm -rf /usr/src/redhat/BUILD/$(PKG)*
--	rm -rf /usr/src/redhat/RPMS/noarch/$(PKG)*
--	rm -rf /usr/src/redhat/SOURCES/$(PKG)*
--	rm -rf /usr/src/redhat/SRPMS/$(PKG)*
--	find . -name "*.pyc" -exec rm -f {} \;
--	find . -name "*~" -exec rm -f {} \;
--
--.PHONY : clean register docs pdf
diff --git a/debian/patches/02-fix-unsecure-cache-path.patch b/debian/patches/02-fix-unsecure-cache-path.patch
deleted file mode 100644
index bb92bec..0000000
--- a/debian/patches/02-fix-unsecure-cache-path.patch
+++ /dev/null
@@ -1,331 +0,0 @@
-Author: Jurko Gospodnetić
-Description: Fix for CVE-2013-2217:
- Use secure temporary directory creation when initializing
- file-based URL cache.
-
- This patch is taken from the suds-jurko fork of suds at
- https://bitbucket.org/jurko/suds. It removes the cache
- files on exit of the calling program.
-
- References:
- https://bitbucket.org/jurko/suds/issue/15/insecure-temporary-directory-use
- https://bitbucket.org/jurko/suds/commits/3126ac3a406c37f9982f01ad0ca4ed42cf9a47cb
- https://bitbucket.org/jurko/suds/commits/aee4b2f0318f4b4545a1da826149edaa2c047460
-Bug: https://bugzilla.redhat.com/show_bug.cgi?id=978696
-Bug-Debian: http://bugs.debian.org/714340
-Forwarded: https://bugzilla.redhat.com/show_bug.cgi?id=978696#c14
---- suds.orig/suds/cache.py	2014-05-25 23:31:24.697791871 +0200
-+++ suds/suds/cache.py	2014-05-25 23:34:02.112407424 +0200
-@@ -1,6 +1,6 @@
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the (LGPL) GNU Lesser General Public License as
--# published by the Free Software Foundation; either version 3 of the 
-+# published by the Free Software Foundation; either version 3 of the
- # License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
-@@ -19,6 +19,8 @@
- """
- 
- import os
-+import tempfile
-+import shutil
- import suds
- from tempfile import gettempdir as tmp
- from suds.transport import *
-@@ -50,7 +52,7 @@
-         @rtype: any
-         """
-         raise Exception('not-implemented')
--    
-+
-     def getf(self, id):
-         """
-         Get a object from the cache by ID.
-@@ -60,7 +62,7 @@
-         @rtype: any
-         """
-         raise Exception('not-implemented')
--    
-+
-     def put(self, id, object):
-         """
-         Put a object into the cache.
-@@ -70,7 +72,7 @@
-         @type object: any
-         """
-         raise Exception('not-implemented')
--    
-+
-     def putf(self, id, fp):
-         """
-         Write a fp into the cache.
-@@ -80,33 +82,33 @@
-         @type fp: file-like object.
-         """
-         raise Exception('not-implemented')
--    
-+
-     def purge(self, id):
-         """
-         Purge a object from the cache by id.
-         @param id: A object ID.
--        @type id: str        
-+        @type id: str
-         """
-         raise Exception('not-implemented')
--    
-+
-     def clear(self):
-         """
-         Clear all objects from the cache.
-         """
-         raise Exception('not-implemented')
--    
-+
- 
- class NoCache(Cache):
-     """
-     The passthru object cache.
-     """
--    
-+
-     def get(self, id):
-         return None
--    
-+
-     def getf(self, id):
-         return None
--    
-+
-     def put(self, id, object):
-         pass
- 
-@@ -119,6 +121,9 @@
-     A file-based URL cache.
-     @cvar fnprefix: The file name prefix.
-     @type fnsuffix: str
-+    @cvar remove_default_location_on_exit: Whether to remove the default cache
-+        location on process exit (default=True).
-+    @type remove_default_location_on_exit: bool
-     @ivar duration: The cached file duration which defines how
-         long the file will be cached.
-     @type duration: (unit, value)
-@@ -127,7 +132,10 @@
-     """
-     fnprefix = 'suds'
-     units = ('months', 'weeks', 'days', 'hours', 'minutes', 'seconds')
--    
-+    __default_location = None
-+    remove_default_location_on_exit = True
-+
-+
-     def __init__(self, location=None, **duration):
-         """
-         @param location: The directory for the cached files.
-@@ -138,12 +146,12 @@
-         @type duration: {unit:value}
-         """
-         if location is None:
--            location = os.path.join(tmp(), 'suds')
-+            location = self.__get_default_location()
-         self.location = location
-         self.duration = (None, 0)
-         self.setduration(**duration)
-         self.checkversion()
--        
-+
-     def fnsuffix(self):
-         """
-         Get the file name suffix
-@@ -151,10 +159,10 @@
-         @rtype: str
-         """
-         return 'gcf'
--        
-+
-     def setduration(self, **duration):
-         """
--        Set the caching duration which defines how long the 
-+        Set the caching duration which defines how long the
-         file will be cached.
-         @param duration: The cached file duration which defines how
-             long the file will be cached.  A duration=0 means forever.
-@@ -167,7 +175,7 @@
-                 raise Exception('must be: %s' % str(self.units))
-             self.duration = arg
-         return self
--    
-+
-     def setlocation(self, location):
-         """
-         Set the location (directory) for the cached files.
-@@ -175,7 +183,20 @@
-         @type location: str
-         """
-         self.location = location
--            
-+
-+    @staticmethod
-+    def __get_default_location():
-+        """
-+        Returns the current process's default cache location folder.
-+        The folder is determined lazily on first call.
-+        """
-+        if not FileCache.__default_location:
-+            tmp = tempfile.mkdtemp("suds-default-cache")
-+            FileCache.__default_location = tmp
-+            import atexit
-+            atexit.register(FileCache.__remove_default_location)
-+        return FileCache.__default_location
-+
-     def mktmp(self):
-         """
-         Make the I{location} directory if it doesn't already exits.
-@@ -186,7 +207,18 @@
-         except:
-             log.debug(self.location, exc_info=1)
-         return self
--    
-+
-+    @staticmethod
-+    def __remove_default_location():
-+        """
-+        Removes the default cache location folder.
-+        This removal may be disabled by setting the
-+        remove_default_location_on_exit FileCache class attribute to False.
-+
-+        """
-+        if FileCache.remove_default_location_on_exit:
-+            shutil.rmtree(FileCache.__default_location, ignore_errors=True)
-+
-     def put(self, id, bfr):
-         try:
-             fn = self.__fn(id)
-@@ -197,7 +229,7 @@
-         except:
-             log.debug(id, exc_info=1)
-             return bfr
--        
-+
-     def putf(self, id, fp):
-         try:
-             fn = self.__fn(id)
-@@ -209,7 +241,7 @@
-         except:
-             log.debug(id, exc_info=1)
-             return fp
--        
-+
-     def get(self, id):
-         try:
-             f = self.getf(id)
-@@ -218,7 +250,7 @@
-             return bfr
-         except:
-             pass
--    
-+
-     def getf(self, id):
-         try:
-             fn = self.__fn(id)
-@@ -241,7 +273,7 @@
-         if expired < dt.now():
-             log.debug('%s expired, deleted', fn)
-             os.remove(fn)
-- 
-+
-     def clear(self):
-         for fn in os.listdir(self.location):
-             if os.path.isdir(fn):
-@@ -249,25 +281,25 @@
-             if fn.startswith(self.fnprefix):
-                 log.debug('deleted: %s', fn)
-                 os.remove(os.path.join(self.location, fn))
--                
-+
-     def purge(self, id):
-         fn = self.__fn(id)
-         try:
-             os.remove(fn)
-         except:
-             pass
--                
-+
-     def open(self, fn, *args):
-         """
-         Open the cache file making sure the directory is created.
-         """
-         self.mktmp()
-         return open(fn, *args)
--    
-+
-     def checkversion(self):
-         path = os.path.join(self.location, 'version')
-         try:
--            
-+
-             f = self.open(path)
-             version = f.read()
-             f.close()
-@@ -277,23 +309,23 @@
-             self.clear()
-             f = self.open(path, 'w')
-             f.write(suds.__version__)
--            f.close()        
--    
-+            f.close()
-+
-     def __fn(self, id):
-         name = id
-         suffix = self.fnsuffix()
-         fn = '%s-%s.%s' % (self.fnprefix, name, suffix)
-         return os.path.join(self.location, fn)
--    
--    
-+
-+
- class DocumentCache(FileCache):
-     """
-     Provides xml document caching.
-     """
--    
-+
-     def fnsuffix(self):
-         return 'xml'
--    
-+
-     def get(self, id):
-         try:
-             fp = FileCache.getf(self, id)
-@@ -303,7 +335,7 @@
-             return p.parse(fp)
-         except:
-             FileCache.purge(self, id)
--    
-+
-     def put(self, id, object):
-         if isinstance(object, Element):
-             FileCache.put(self, id, str(object))
-@@ -317,10 +349,10 @@
-     @type protocol: int
-     """
-     protocol = 2
--    
-+
-     def fnsuffix(self):
-         return 'px'
--    
-+
-     def get(self, id):
-         try:
-             fp = FileCache.getf(self, id)
-@@ -330,7 +362,7 @@
-                 return pickle.load(fp)
-         except:
-             FileCache.purge(self, id)
--    
-+
-     def put(self, id, object):
-         bfr = pickle.dumps(object, self.protocol)
-         FileCache.put(self, id, bfr)
diff --git a/debian/patches/03-fix-timezone-handling.patch b/debian/patches/03-fix-timezone-handling.patch
deleted file mode 100644
index fbf866a..0000000
--- a/debian/patches/03-fix-timezone-handling.patch
+++ /dev/null
@@ -1,898 +0,0 @@
-From: gwalker (Glen Walker)
-Date: Sat Sep  4 11:48:00 EEST 2010
-Subject: Fix deficiencies in suds.sax.date module
-Bug: https://fedorahosted.org/suds/ticket/353
-
-There are several problems with the implementation of suds.sax.date:
-
-    * The ​timezone handling capabilities of the Python built-in classes
-    datetime.datetime and ​datetime.time are not used. When an xsd:dateTime or
-    xsd:time value is received an attempt is made to convert the value to
-    localtime, and then timezone naive datetime and time objects are
-    created. Any timezone information is not provided to the caller.
-
-    * Only the hour portion of a timezone offset is handled, and any minutes
-    are ignored. Multiple timezones do ​exist with an offset from UTC that is
-    not a exact multiple of 1 hour, so the minute part should definitely be
-    taken into account. (Asia/Kabul? +04:30, Australia/Adelaide? +09:30,
-    America/Caracas? -04:30, etc).
-
-    * Times are adjusted incorrectly if the local timezone has daylight
-    savings, as suds.sax.date does not take daylight savings into account.
-
-    * Milliseconds are handled incorrectly if an xsd:dateTime or xsd:time
-    value is received where the sub-second component is not exactly 6 digits
-    long.
-
-There are also a couple of small errors in suds.xsd.sxbuiltin related to
-suds.sax.date. In the classes XTime and XDateTime the handling of types does
-not match the corresponding handling in suds.sax.date.
-
-Attached is a replacement module for suds.sax.date that handles timezones
-correctly, populating the tzinfo property of datetime.datetime and
-datetime.time values with an appropriate ​datetime.tzinfo derived object. This
-allows the caller to easily convert results to localtime or UTC (using the new
-suds.sax.date.LocalTimezone or suds.sax.date.UtcTimezone classes) or arbitrary
-timezones (using ​pytz or similar) as required by their application. It does
-not make any assumptions about the timezone of xsd:dateTime and xsd:time
-values where no offset was included - this is left up to the caller.
-
----
- suds/sax/date.py      |  697 +++++++++++++++++++++++++++----------------------
- suds/wsse.py          |   12 +-
- suds/xsd/sxbuiltin.py |   10 +-
- 3 files changed, 394 insertions(+), 325 deletions(-)
-
-diff --git a/suds/sax/date.py b/suds/sax/date.py
-index 6e31c4c..bb2d031 100644
---- a/suds/sax/date.py
-+++ b/suds/sax/date.py
-@@ -1,6 +1,6 @@
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the (LGPL) GNU Lesser General Public License as
--# published by the Free Software Foundation; either version 3 of the 
-+# published by the Free Software Foundation; either version 3 of the
- # License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
-@@ -12,367 +12,436 @@
- # You should have received a copy of the GNU Lesser General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--# written by: Nathan Van Gheem (vangheem at gmail.com)
-+# written by: Glen Walker
-+# based on code by: Nathan Van Gheem (vangheem at gmail.com)
- 
- """
--The I{xdate} module provides classes for converstion
--between XML dates and python objects.
-+The I{date} module provides classes for converstion between XML dates and
-+Python objects.
- """
- 
--from logging import getLogger
--from suds import *
--from suds.xsd import *
-+
-+__all__ = ('Date', 'Time', 'DateTime', 'FixedOffsetTimezone', 'UtcTimezone',
-+           'LocalTimezone')
-+
-+
- import time
--import datetime as dt
-+import datetime
- import re
- 
--log = getLogger(__name__)
- 
-+SNIPPET_DATE = r'(?P<year>\d{1,})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
-+SNIPPET_TIME = r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})' + \
-+               r'(?:\.(?P<subsecond>\d+))?'
-+SNIPPET_ZONE = r'(?:(?P<tz_sign>[-+])(?P<tz_hour>\d{1,2})' + \
-+               r'(?::?(?P<tz_minute>\d{1,2})(?::?(?P<tz_second>\d{1,2}))?)?)' + \
-+               r'|(?P<tz_utc>[Zz])'
-+
-+PATTERN_DATE = r'^%s(?:%s)?$' % (SNIPPET_DATE, SNIPPET_ZONE)
-+PATTERN_TIME = r'^%s(?:%s)?$' % (SNIPPET_TIME, SNIPPET_ZONE)
-+PATTERN_DATETIME = r'^%s[T ]%s(?:%s)?$' % (SNIPPET_DATE, SNIPPET_TIME,
-+                                           SNIPPET_ZONE)
-+
-+RE_DATE = re.compile(PATTERN_DATE)
-+RE_TIME = re.compile(PATTERN_TIME)
-+RE_DATETIME = re.compile(PATTERN_DATETIME)
-+
-+
-+class Date(object):
-+    """An XML date object supporting the xsd:date datatype.
-+
-+    @ivar value: The object value.
-+    @type value: B{datetime}.I{date}
- 
--class Date:
--    """
--    An XML date object.
--    Supported formats:
--        - YYYY-MM-DD
--        - YYYY-MM-DD(z|Z)
--        - YYYY-MM-DD+06:00
--        - YYYY-MM-DD-06:00
--    @ivar date: The object value.
--    @type date: B{datetime}.I{date}
-     """
--    def __init__(self, date):
--        """
--        @param date: The value of the object.
--        @type date: (date|str)
--        @raise ValueError: When I{date} is invalid.
--        """
--        if isinstance(date, dt.date):
--            self.date = date
--            return
--        if isinstance(date, basestring):
--            self.date = self.__parse(date)
--            return
--        raise ValueError, type(date)
--    
--    def year(self):
--        """
--        Get the I{year} component.
--        @return: The year.
--        @rtype: int
--        """
--        return self.date.year
--    
--    def month(self):
--        """
--        Get the I{month} component.
--        @return: The month.
--        @rtype: int
--        """
--        return self.date.month
--    
--    def day(self):
--        """
--        Get the I{day} component.
--        @return: The day.
--        @rtype: int
--        """
--        return self.date.day
--        
--    def __parse(self, s):
-+    __slots__ = ('value', )
-+
-+    def __init__(self, value):
-+        """Constructor.
-+
-+        @param value: The date value of the object.
-+        @type value: (datetime.date|str)
-+        @raise ValueError: When I{value} is invalid.
-+
-         """
--        Parse the string date.
--        Supported formats:
--            - YYYY-MM-DD
--            - YYYY-MM-DD(z|Z)
--            - YYYY-MM-DD+06:00
--            - YYYY-MM-DD-06:00
--        Although, the TZ is ignored because it's meaningless
--        without the time, right?
--        @param s: A date string.
--        @type s: str
-+        if isinstance(value, datetime.date):
-+            self.value = value
-+        elif isinstance(value, basestring):
-+            self.value = self.parse(value)
-+        else:
-+            raise ValueError('invalid type for Date(): %s' % (type(value), ))
-+
-+    @staticmethod
-+    def parse(value):
-+        """Parse the string date.
-+
-+        This supports the subset of ISO8601 used by xsd:date, but is lenient
-+        with what is accepted, handling most reasonable syntax.
-+
-+        Any timezone is parsed but ignored  because a) it's meaningless without
-+        a time and b) B{datetime}.I{date} does not support a tzinfo property.
-+
-+        @param value: A date string.
-+        @type value: str
-         @return: A date object.
--        @rtype: I{date}
-+        @rtype: B{datetime}.I{date}
-+
-         """
--        try:
--            year, month, day = s[:10].split('-', 2)
--            year = int(year)
--            month = int(month)
--            day = int(day)
--            return dt.date(year, month, day)
--        except:
--            log.debug(s, exec_info=True)
--            raise ValueError, 'Invalid format "%s"' % s
--        
-+        match_result = RE_DATE.match(value)
-+        if match_result is None:
-+           raise ValueError('date data has invalid format "%s"' % (value, ))
-+
-+        value = date_from_match(match_result)
-+
-+        return value
-+
-     def __str__(self):
-         return unicode(self)
--    
-+
-     def __unicode__(self):
--        return self.date.isoformat()
-+        return self.value.isoformat()
- 
- 
--class Time:
--    """
--    An XML time object.
--    Supported formats:
--        - HH:MI:SS
--        - HH:MI:SS(z|Z)
--        - HH:MI:SS.ms
--        - HH:MI:SS.ms(z|Z)
--        - HH:MI:SS(+|-)06:00
--        - HH:MI:SS.ms(+|-)06:00
--    @ivar tz: The timezone
--    @type tz: L{Timezone}
--    @ivar date: The object value.
--    @type date: B{datetime}.I{time}
-+class Time(object):
-+    """An XML time object supporting the xsd:time datatype.
-+
-+    @ivar time: The object value.
-+    @type time: B{datetime}.I{time}
-+
-     """
--    
--    def __init__(self, time, adjusted=True):
--        """
--        @param time: The value of the object.
--        @type time: (time|str)
--        @param adjusted: Adjust for I{local} Timezone.
--        @type adjusted: boolean
--        @raise ValueError: When I{time} is invalid.
--        """
--        self.tz = Timezone()
--        if isinstance(time, dt.time):
--            self.time = time
--            return
--        if isinstance(time, basestring):
--            self.time = self.__parse(time)
--            if adjusted:
--                self.__adjust()
--            return
--        raise ValueError, type(time)
--    
--    def hour(self):
--        """
--        Get the I{hour} component.
--        @return: The hour.
--        @rtype: int
--        """
--        return self.time.hour
--    
--    def minute(self):
--        """
--        Get the I{minute} component.
--        @return: The minute.
--        @rtype: int
--        """
--        return self.time.minute
--    
--    def second(self):
--        """
--        Get the I{seconds} component.
--        @return: The seconds.
--        @rtype: int
-+    __slots__ = ('value', )
-+
-+    def __init__(self, value):
-+        """Constructor.
-+
-+        @param value: The time value of the object.
-+        @type value: (dateime.time|str)
-+        @raise ValueError: When I{value} is invalid.
-+
-         """
--        return self.time.second
--    
--    def microsecond(self):
-+        if isinstance(value, datetime.time):
-+            self.value = value
-+        elif isinstance(value, basestring):
-+            self.value = self.parse(value)
-+        else:
-+            raise ValueError('invalid type for Time(): %s' % (type(value), ))
-+
-+    @staticmethod
-+    def parse(value):
-+        """Parse the string date.
-+
-+        This supports the subset of ISO8601 used by xsd:time, but is lenient
-+        with what is accepted, handling most reasonable syntax.
-+
-+        @param value: A time string.
-+        @type value: str
-+        @return: A time object.
-+        @rtype: B{datetime}.I{time}
-+
-         """
--        Get the I{microsecond} component.
--        @return: The microsecond.
--        @rtype: int
-+        match_result = RE_TIME.match(value)
-+        if match_result is None:
-+           raise ValueError('date data has invalid format "%s"' % (value, ))
-+
-+        date = time_from_match(match_result)
-+        tzinfo = tzinfo_from_match(match_result)
-+
-+        value = date.replace(tzinfo=tzinfo)
-+
-+        return value
-+
-+    def __str__(self):
-+        return unicode(self)
-+
-+    def __unicode__(self):
-+        return self.time.isoformat()
-+
-+
-+class DateTime(object):
-+    """An XML datetime object supporting the xsd:dateTime datatype.
-+
-+    @ivar value: The object value.
-+    @type value: B{datetime}.I{datetime}
-+
-+    """
-+    __slots__ = ('value', )
-+
-+    def __init__(self, value):
-+        """Constructor.
-+
-+        @param value: The datetime value of the object.
-+        @type value: (datetime.datetime|str)
-+        @raise ValueError: When I{value} is invalid.
-+
-         """
--        return self.time.microsecond
--    
--    def __adjust(self):
-+        if isinstance(value, datetime.datetime):
-+            self.value = value
-+        elif isinstance(value, basestring):
-+            self.value = self.parse(value)
-+        else:
-+            raise ValueError('invalid type for DateTime(): %s' % (type(value), ))
-+
-+    @staticmethod
-+    def parse(value):
-+        """Parse the string datetime.
-+
-+        This supports the subset of ISO8601 used by xsd:dateTime, but is lenient
-+        with what is accepted, handling most reasonable syntax.
-+
-+        @param value: A datetime string.
-+        @type value: str
-+        @return: A datetime object.
-+        @rtype: B{datetime}.I{datetime}
-+
-         """
--        Adjust for TZ offset.
-+        match_result = RE_DATETIME.match(value)
-+        if match_result is None:
-+           raise ValueError('date data has invalid format "%s"' % (value, ))
-+
-+        date = date_from_match(match_result)
-+        time = time_from_match(match_result)
-+        tzinfo = tzinfo_from_match(match_result)
-+
-+        value = datetime.datetime.combine(date, time)
-+        value = value.replace(tzinfo=tzinfo)
-+
-+        return value
-+
-+    def __str__(self):
-+        return unicode(self)
-+
-+    def __unicode__(self):
-+        return self.value.isoformat()
-+
-+
-+class FixedOffsetTimezone(datetime.tzinfo):
-+    """A timezone with a fixed offset and no daylight savings adjustment.
-+
-+    http://docs.python.org/library/datetime.html#datetime.tzinfo
-+
-+    """
-+
-+    def __init__(self, offset):
-+        """Constructor.
-+
-+        @ivar offset: The fixed offset of the timezone.
-+        @type offset: B{datetime}.I{timedelta}
-+
-         """
--        if hasattr(self, 'offset'):
--            today = dt.date.today()
--            delta = self.tz.adjustment(self.offset)
--            d = dt.datetime.combine(today, self.time)
--            d = ( d + delta )
--            self.time = d.time()
--        
--    def __parse(self, s):
-+        self.__offset = offset
-+
-+    def utcoffset(self, dt):
-         """
--        Parse the string date.
--        Patterns:
--            - HH:MI:SS
--            - HH:MI:SS(z|Z)
--            - HH:MI:SS.ms
--            - HH:MI:SS.ms(z|Z)
--            - HH:MI:SS(+|-)06:00
--            - HH:MI:SS.ms(+|-)06:00
--        @param s: A time string.
--        @type s: str
--        @return: A time object.
--        @rtype: B{datetime}.I{time}
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
-+
-         """
--        try:
--            offset = None
--            part = Timezone.split(s)
--            hour, minute, second = part[0].split(':', 2)
--            hour = int(hour)
--            minute = int(minute)
--            second, ms = self.__second(second)
--            if len(part) == 2:
--                self.offset = self.__offset(part[1])
--            if ms is None:
--                return dt.time(hour, minute, second)
--            else:
--                return dt.time(hour, minute, second, ms)
--        except:
--            log.debug(s, exec_info=True)
--            raise ValueError, 'Invalid format "%s"' % s
--        
--    def __second(self, s):
-+        return self.__offset
-+
-+    def tzname(self, dt):
-         """
--        Parse the seconds and microseconds.
--        The microseconds are truncated to 999999 due to a restriction in
--        the python datetime.datetime object.
--        @param s: A string representation of the seconds.
--        @type s: str
--        @return: Tuple of (sec,ms)
--        @rtype: tuple.
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
-+
-         """
--        part = s.split('.')
--        if len(part) > 1:
--            return (int(part[0]), int(part[1][:6]))
-+        sign = '+'
-+        if self.__offset < datetime.timedelta():
-+            sign = '-'
-+
-+        # total_seconds was introduced in Python 2.7
-+        if hasattr(self.__offset, 'total_seconds'):
-+            total_seconds = self.__offset.total_seconds()
-+        else:
-+            total_seconds = (self.__offset.days * 24 * 60 * 60) + \
-+                            (self.__offset.seconds) + \
-+                            (self.__offset.microseconds / 1000000.0)
-+
-+        hours = total_seconds // (60 * 60)
-+        total_seconds -= hours * 60 * 60
-+
-+        minutes = total_seconds // 60
-+        total_seconds -= minutes * 60
-+
-+        seconds = total_seconds // 1
-+        total_seconds -= seconds
-+
-+        if seconds:
-+            return '%s%02d:%02d:%02d' % (sign, hours, minutes, seconds)
-         else:
--            return (int(part[0]), None)
--        
--    def __offset(self, s):
-+            return '%s%02d:%02d' % (sign, hours, minutes)
-+
-+    def dst(self, dt):
-         """
--        Parse the TZ offset.
--        @param s: A string representation of the TZ offset.
--        @type s: str
--        @return: The signed offset in hours.
--        @rtype: str
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
-+
-         """
--        if len(s) == len('-00:00'):
--            return int(s[:3])
--        if len(s) == 0:
--            return self.tz.local
--        if len(s) == 1:
--            return 0
--        raise Exception()
-+        return datetime.timedelta(0)
- 
-     def __str__(self):
-         return unicode(self)
--    
-+
-     def __unicode__(self):
--        time = self.time.isoformat()
--        if self.tz.local:
--            return '%s%+.2d:00' % (time, self.tz.local)
--        else:
--            return '%sZ' % time
-+        return 'FixedOffsetTimezone %s' % (self.tzname(None), )
-+
-+
-+class UtcTimezone(FixedOffsetTimezone):
-+    """The UTC timezone.
- 
-+    http://docs.python.org/library/datetime.html#datetime.tzinfo
- 
--class DateTime(Date,Time):
-     """
--    An XML time object.
--    Supported formats:
--        - YYYY-MM-DDB{T}HH:MI:SS
--        - YYYY-MM-DDB{T}HH:MI:SS(z|Z)
--        - YYYY-MM-DDB{T}HH:MI:SS.ms
--        - YYYY-MM-DDB{T}HH:MI:SS.ms(z|Z)
--        - YYYY-MM-DDB{T}HH:MI:SS(+|-)06:00
--        - YYYY-MM-DDB{T}HH:MI:SS.ms(+|-)06:00
--    @ivar datetime: The object value.
--    @type datetime: B{datetime}.I{datedate}
-+
-+    def __init__(self):
-+        """Constructor."""
-+        FixedOffsetTimezone.__init__(self, datetime.timedelta(0))
-+
-+    def tzname(self, dt):
-+        """
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
-+
-+        """
-+        return 'UTC'
-+
-+    def __str__(self):
-+        return unicode(self)
-+
-+    def __unicode__(self):
-+        return 'UtcTimezone'
-+
-+
-+class LocalTimezone(datetime.tzinfo):
-+    """The local timezone of the operating system.
-+
-+    http://docs.python.org/library/datetime.html#datetime.tzinfo
-+
-     """
--    def __init__(self, date):
-+
-+    def __init__(self):
-+        """Constructor."""
-+        self.__offset = datetime.timedelta(seconds=-time.timezone)
-+        if time.daylight:
-+            self.__dst_offset = datetime.timedelta(seconds=-time.altzone)
-+        else:
-+            self.__dst_offset = None
-+
-+    def utcoffset(self, dt):
-+        """
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
-+
-+        """
-+        if self.__is_daylight_time(dt):
-+            return self.__dst_offset
-+        else:
-+            return self.__offset
-+
-+    def dst(self, dt):
-         """
--        @param date: The value of the object.
--        @type date: (datetime|str)
--        @raise ValueError: When I{tm} is invalid.
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
-+
-         """
--        if isinstance(date, dt.datetime):
--            Date.__init__(self, date.date())
--            Time.__init__(self, date.time())
--            self.datetime = \
--                dt.datetime.combine(self.date, self.time)
--            return
--        if isinstance(date, basestring):
--            part = date.split('T')
--            Date.__init__(self, part[0])
--            Time.__init__(self, part[1], 0)
--            self.datetime = \
--                dt.datetime.combine(self.date, self.time)
--            self.__adjust()
--            return
--        raise ValueError, type(date)
--    
--    def __adjust(self):
-+        if self.__is_daylight_time(dt):
-+            return self.__dst_offset - self.__offset
-+        else:
-+            return datetime.timedelta(0)
-+
-+    def tzname(self, dt):
-         """
--        Adjust for TZ offset.
-+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
-+
-         """
--        if not hasattr(self, 'offset'):
--            return
--        delta = self.tz.adjustment(self.offset)
--        try:
--            d = ( self.datetime + delta )
--            self.datetime = d
--            self.date = d.date()
--            self.time = d.time()
--        except OverflowError:
--            log.warn('"%s" caused overflow, not-adjusted', self.datetime)
-+        if self.__is_daylight_time(dt):
-+            return time.tzname[1]
-+        else:
-+            return time.tzname[0]
-+
-+    def __is_daylight_time(self, dt):
-+        if not time.daylight:
-+            return False
-+        time_tuple = dt.replace(tzinfo=None).timetuple()
-+        time_tuple = time.localtime(time.mktime(time_tuple))
-+        return time_tuple.tm_isdst > 0
- 
-     def __str__(self):
-         return unicode(self)
--    
-+
-     def __unicode__(self):
--        s = []
--        s.append(Date.__unicode__(self))
--        s.append(Time.__unicode__(self))
--        return 'T'.join(s)
--    
--    
--class UTC(DateTime):
--    """
--    Represents current UTC time.
-+        dt = datetime.datetime.now()
-+        return 'LocalTimezone %s offset: %s dst: %s' \
-+            % (self.tzname(dt), self.utcoffset(dt), self.dst(dt))
-+
-+
-+def date_from_match(match_object):
-+    """Create a date object from a regular expression match.
-+
-+    The regular expression match is expected to be from RE_DATE or RE_DATETIME.
-+
-+    @param match_object: The regular expression match.
-+    @type value: B{re}.I{MatchObject}
-+    @return: A date object.
-+    @rtype: B{datetime}.I{date}
-+
-     """
--    
--    def __init__(self, date=None):
--        if date is None:
--            date = dt.datetime.utcnow()
--        DateTime.__init__(self, date)
--        self.tz.local = 0
--    
--    
--class Timezone:
-+    year = int(match_object.group('year'))
-+    month = int(match_object.group('month'))
-+    day = int(match_object.group('day'))
-+
-+    return datetime.date(year, month, day)
-+
-+
-+def time_from_match(match_object):
-+    """Create a time object from a regular expression match.
-+
-+    The regular expression match is expected to be from RE_TIME or RE_DATETIME.
-+
-+    @param match_object: The regular expression match.
-+    @type value: B{re}.I{MatchObject}
-+    @return: A date object.
-+    @rtype: B{datetime}.I{time}
-+
-     """
--    Timezone object used to do TZ conversions
--    @cvar local: The (A) local TZ offset.
--    @type local: int
--    @cvar patten: The regex patten to match TZ.
--    @type patten: re.Pattern
-+    hour = int(match_object.group('hour'))
-+    minute = int(match_object.group('minute'))
-+    second = int(match_object.group('second'))
-+    subsecond = match_object.group('subsecond')
-+
-+    microsecond = 0
-+    if subsecond is not None:
-+        subsecond_denominator = 10.0 ** len(subsecond)
-+        subsecond = int(subsecond)
-+        microsecond = subsecond * (1000000 / subsecond_denominator)
-+        microsecond = int(round(microsecond))
-+
-+    return datetime.time(hour, minute, second, microsecond)
-+
-+
-+def tzinfo_from_match(match_object):
-+    """Create a timezone information object from a regular expression match.
-+
-+    The regular expression match is expected to be from RE_DATE, RE_TIME or
-+    RE_DATETIME.
-+
-+    @param match_object: The regular expression match.
-+    @type value: B{re}.I{MatchObject}
-+    @return: A timezone information object.
-+    @rtype: B{datetime}.I{tzinfo}
-+
-     """
--    
--    pattern = re.compile('([zZ])|([\-\+][0-9]{2}:[0-9]{2})')
--    
--    LOCAL = ( 0-time.timezone/60/60 )
--
--    def __init__(self, offset=None):
--        if offset is None:
--            offset = self.LOCAL
--        self.local = offset
--    
--    @classmethod
--    def split(cls, s):
--        """
--        Split the TZ from string.
--        @param s: A string containing a timezone
--        @type s: basestring
--        @return: The split parts.
--        @rtype: tuple
--        """
--        m = cls.pattern.search(s)
--        if m is None:
--            return (s,)
--        x = m.start(0)
--        return (s[:x], s[x:])
-+    tz_utc = match_object.group('tz_utc')
-+    tz_sign = match_object.group('tz_sign')
-+    tz_hour = int(match_object.group('tz_hour') or 0)
-+    tz_minute = int(match_object.group('tz_minute') or 0)
-+    tz_second = int(match_object.group('tz_second') or 0)
- 
--    def adjustment(self, offset):
--        """
--        Get the adjustment to the I{local} TZ.
--        @return: The delta between I{offset} and local TZ.
--        @rtype: B{datetime}.I{timedelta}
--        """
--        delta = ( self.local - offset )
--        return dt.timedelta(hours=delta)
-+    tzinfo = None
-+    if tz_utc is not None:
-+        tzinfo = UtcTimezone()
-+    elif tz_sign is not None:
-+        tz_delta = datetime.timedelta(hours=tz_hour,
-+                                      minutes=tz_minute,
-+                                      seconds=tz_second)
-+        if tz_delta == datetime.timedelta():
-+            tzinfo = UtcTimezone()
-+        else:
-+            tz_multiplier = int('%s1' % (tz_sign, ))
-+            tz_delta = tz_multiplier * tz_delta
-+            tzinfo = FixedOffsetTimezone(tz_delta)
-+
-+    return tzinfo
-\ No newline at end of file
-diff --git a/suds/wsse.py b/suds/wsse.py
-index 2a697c1..6cd70a0 100644
---- a/suds/wsse.py
-+++ b/suds/wsse.py
-@@ -22,7 +22,7 @@ from logging import getLogger
- from suds import *
- from suds.sudsobject import Object
- from suds.sax.element import Element
--from suds.sax.date import UTC
-+from suds.sax.date import DateTime, UtcTimezone
- from datetime import datetime, timedelta
- 
- try:
-@@ -90,11 +90,11 @@ class Token(Object):
-     
-     @classmethod
-     def utc(cls):
--        return datetime.utcnow()
-+        return datetime.utcnow().replace(tzinfo=UtcTimezone())
-     
-     @classmethod
-     def sysdate(cls):
--        utc = UTC()
-+        utc = DateTime(self.utc())
-         return str(utc)
-     
-     def __init__(self):
-@@ -178,7 +178,7 @@ class UsernameToken(Token):
-             root.append(n)
-         if self.created is not None:
-             n = Element('Created', ns=wsuns)
--            n.setText(str(UTC(self.created)))
-+            n.setText(str(DateTime(self.created)))
-             root.append(n)
-         return root
- 
-@@ -204,9 +204,9 @@ class Timestamp(Token):
-     def xml(self):
-         root = Element("Timestamp", ns=wsuns)
-         created = Element('Created', ns=wsuns)
--        created.setText(str(UTC(self.created)))
-+        created.setText(str(DateTime(self.created)))
-         expires = Element('Expires', ns=wsuns)
--        expires.setText(str(UTC(self.expires)))
-+        expires.setText(str(DateTime(self.expires)))
-         root.append(created)
-         root.append(expires)
-         return root
-\ No newline at end of file
-diff --git a/suds/xsd/sxbuiltin.py b/suds/xsd/sxbuiltin.py
-index f8cf428..a3543bd 100644
---- a/suds/xsd/sxbuiltin.py
-+++ b/suds/xsd/sxbuiltin.py
-@@ -138,7 +138,7 @@ class XDate(XBuiltin):
-     def translate(self, value, topython=True):
-         if topython:
-             if isinstance(value, basestring) and len(value):
--                return Date(value).date
-+                return Date(value).value
-             else:
-                 return None
-         else:
-@@ -156,11 +156,11 @@ class XTime(XBuiltin):
-     def translate(self, value, topython=True):
-         if topython:
-             if isinstance(value, basestring) and len(value):
--                return Time(value).time
-+                return Time(value).value
-             else:
-                 return None
-         else:
--            if isinstance(value, dt.date):
-+            if isinstance(value, dt.time):
-                 return str(Time(value))
-             else:
-                 return value
-@@ -174,11 +174,11 @@ class XDateTime(XBuiltin):
-     def translate(self, value, topython=True):
-         if topython:
-             if isinstance(value, basestring) and len(value):
--                return DateTime(value).datetime
-+                return DateTime(value).value
-             else:
-                 return None
-         else:
--            if isinstance(value, dt.date):
-+            if isinstance(value, dt.datetime):
-                 return str(DateTime(value))
-             else:
-                 return value
diff --git a/debian/patches/series b/debian/patches/series
deleted file mode 100644
index 8715974..0000000
--- a/debian/patches/series
+++ /dev/null
@@ -1,3 +0,0 @@
-01-remove-makefile
-02-fix-unsecure-cache-path.patch
-03-fix-timezone-handling.patch
commit 7a949da4ecf683ef3267c365fff622110c17aa3e
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:47:49 2014 +0200

    Adding Build-Depends.

diff --git a/debian/control b/debian/control
index dd02d7d..86ef56d 100644
--- a/debian/control
+++ b/debian/control
@@ -4,7 +4,8 @@ Priority: optional
 Maintainer: Debian Tryton Maintainers <maintainers at debian.tryton.org>
 Uploaders: Mathias Behrle <mathiasb at m9s.biz>
 Build-Depends:
- debhelper (>= 9), python (>= 2.6.6-3~), python-setuptools, dh-python
+ debhelper (>= 9), python (>= 2.6.6-3~), python-setuptools, dh-python,
+ python-pytest
 Standards-Version: 3.9.5
 Homepage: http://www.fedorahosted.org/suds/
 Vcs-Browser: http://anonscm.debian.org/gitweb/?p=tryton/suds.git
commit 1758192d752aacb70008454196af152255b9b4e9
Author: Mathias Behrle <mathiasb at m9s.biz>
Date:   Mon Jun 30 18:04:01 2014 +0200

    Adding upstream version 0.6.

diff --git a/HACKING.rst b/HACKING.rst
new file mode 100644
index 0000000..1940d16
--- /dev/null
+++ b/HACKING.rst
@@ -0,0 +1,543 @@
+GENERAL DEVELOPMENT NOTES
+=================================================
+
+Project's sources are accessible from a `Mercurial version control repository
+<http://bitbucket.org/jurko/suds>`_ hosted at BitBucket.
+
+Project development should be tracked in the ``TODO.txt`` file.
+
+* Exact formatting is not important as long as its content is kept formatted
+  consistently.
+* Done tasks should be marked as such and not deleted.
+
+Testing:
+
+* ``pytest`` testing framework needed to run unit tests.
+* To run the tests using Python 3 first process them and the rest of the library
+  sources using the Python ``py2to3`` conversion tool.
+* For more detailed information see the `DEVELOPMENT & TESTING ENVIRONMENT`_
+  section below.
+
+Reproducing problematic use cases:
+
+* Failing web service processing examples can be easily packaged as reproducible
+  test cases using the suds library 'message & reply injection' technique.
+* Some things you can achieve using this technique (for examples, see existing
+  project unit tests):
+
+  * Create a client object based on a fixed WSDL string.
+  * Have a client object send a fixed request string without having it construct
+    one based on the loaded WSDL schema and received arguments.
+  * Have a client object process a fixed reply string without having it send a
+    request to an actual external web service.
+
+Base sources should remain Python 2.x compatible. Since the original project
+states aiming for Python 2.4 compatibility we should do so as well.
+
+Python features that need to be avoided for compatibility with older Python
+versions:
+
+* Features introduced in Python 2.5.
+
+  * ``any`` & ``all`` functions.
+  * ``with`` statement.
+  * ``try``/``except``/``finally`` blocks.
+
+    * Prior to this Python release, code like the following::
+
+        try:
+            A
+        except XXX:
+            B
+        finally:
+            C
+
+      was considered illegal and needed to be written using nested ``try``
+      blocks as in::
+
+        try:
+            try:
+                A
+            except XXX:
+                B
+        finally:
+            C
+
+  * ``yield`` expression inside a ``try`` block with a ``finally`` clause.
+
+    * Prior to this Python release, code like the following::
+
+        try:
+            yield x
+        finally:
+            do_something()
+
+      is considered illegal, but can be replaced with legal code similar to the
+      following::
+
+        try:
+            yield x
+        except:
+            do_something()
+            raise
+        do_something()
+
+* Features introduced in Python 2.6.
+
+  * ``bytes`` type.
+  * Byte literals, e.g. ``b"quack"``.
+  * String ``format()`` method.
+
+* Features introduced in Python 2.7.
+
+  * Dictionary & set comprehensions.
+  * Set literals.
+
+Handling Python 3 related patches applicable to the original suds development
+project.
+
+* Originally synchronized with the `Mercurial patch queue
+  <http://bitbucket.org/bernh/suds-python-3-patches>`_ maintained by Bernhard
+  Leiner.
+* Used to be kept synchronized with the originating queue but no longer. It
+  became more and more difficult to keep them synchronized as our work
+  progressed and both the original suds project and this patch queue seem dead
+  otherwise.
+* Used to be committed first to the 'Python 3 support' branch and then merged
+  back to the trunk from there.
+
+External documentation:
+
+* SOAP
+
+  * http://www.w3.org/TR/soap
+
+  * Version 1.1.
+
+    * http://www.w3.org/TR/2000/NOTE-SOAP-20000508
+
+  * Version 1.2.
+
+    * Part0: Primer
+
+      * http://www.w3.org/TR/2007/REC-soap12-part0-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part0-20070427-errata.html
+
+    * Part1: Messaging Framework
+
+      * http://www.w3.org/TR/2007/REC-soap12-part1-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part1-20070427-errata.html
+
+    * Part2: Adjuncts
+
+      * http://www.w3.org/TR/2007/REC-soap12-part2-20070427
+      * Errata: http://www.w3.org/2007/04/REC-soap12-part2-20070427-errata.html
+
+    * Specification Assertions and Test Collection
+
+      * http://www.w3.org/TR/2007/REC-soap12-testcollection-20070427
+      * Errata:
+        http://www.w3.org/2007/04/REC-soap12-testcollection-20070427-errata.html
+
+* WS-I Basic Profile 1.1
+
+  * http://www.ws-i.org/Profiles/BasicProfile-1.1.html
+
+* WSDL 1.1
+
+  * http://www.w3.org/TR/wsdl
+
+* XML Schema
+
+  * Part 0: Primer Second Edition - http://www.w3.org/TR/xmlschema-0
+
+    * Non-normative document intended to provide an easily readable description
+      of the XML Schema facilities, and is oriented towards quickly
+      understanding how to create schemas using the XML Schema language.
+
+  * Part 1: Structures - http://www.w3.org/TR/xmlschema-1
+  * Part 2: Datatypes - http://www.w3.org/TR/xmlschema-2
+
+For additional design, research & development project notes see the project's
+``notes/`` folder.
+
+
+TOP-LEVEL FILES & FOLDERS
+=================================================
+
+| .hg/
+| .hgignore
+| .hgtags
+
+* Mercurial version control related data.
+
+| build/
+| dist/
+| suds_jurko.egg-info/
+
+* Folders created during project setup procedure (build/install).
+
+| notes/
+
+* Internal project design, research & development notes.
+
+| suds/
+
+* Basic project source code.
+
+| tests/
+
+* Project test code.
+
+| MANIFEST.in
+
+* Build system configuration file listing the files to be included in the
+  project's source distribution packages in addition to those automatically
+  added to those packages by the used package preparation system.
+
+| HACKING.rst
+| LICENSE.txt
+| README.txt
+| TODO.txt
+
+* Internal project documentation.
+
+| run_all_tests.cmd
+
+* Basic development script for running the full project test suite using
+  multiple Python interpreter versions on a Windows development machine.
+
+| setup.cfg
+
+* Basic project Python configuration.
+
+| setup.py
+
+* Standard Python project setup script.
+
+* Usage examples:
+
+  ``setup.py --help``
+    show detailed usage information
+  ``setup.py build``
+    build the project
+  ``setup.py develop``
+    prepare the development environment (add the project folder to the
+    Python module search path)
+  ``setup.py install``
+    build & install the project
+  ``setup.py register``
+    register a project release at PyPI
+  ``setup.py sdist``
+    prepare a source distribution
+  ``setup.py test``
+    run the project's test suite (requires ``pytest``)
+  ``setup.py upload``
+    upload prepared packages to PyPI
+
+
+RELEASE PROCEDURE
+=================================================
+
+1. Document the release correctly in ``README.rst``.
+
+2. Test the project build with the latest available ``setuptools`` project and
+   update the ``ez_setup.py`` ``setuptools`` installation script as needed.
+
+  * Chosen ``setuptools`` version needs to support all the Python interpreter
+    versions supported by our project.
+
+  * ``setuptools`` version 2.0 dropped support for Python 2.4 & 2.5.
+
+3. Version identification.
+
+  * Remove the ``(development)`` suffix for official release builds.
+
+4. Tag in Hg.
+
+  * Name the tag like ``release-<version-info>``, e.g. ``release-0.5``.
+
+5. Prepare official releases based only on tagged commits.
+
+  * Official releases should always be prepared based on tagged revisions with
+    no local changes in the used sandbox.
+  * Prepare source distribution packages (both .zip & .tar.bz2 formats),
+    register the new release at PyPI and upload the prepared source packages.
+
+    * Run ``setup.py sdist register upload``.
+
+  * Upload the prepared source package to the project site.
+
+    * Use the BitBucket project web interface.
+
+6. Next development version identification.
+
+  * Bump up the forked project version counter.
+  * Add back the ``(development)`` suffix, e.g. as in ``0.5 (development)``.
+
+7. Notify whomever the new release might concern.
+
+
+DEVELOPMENT & TESTING ENVIRONMENT
+=================================================
+
+In all command-line examples below pyX, pyXY & pyXYZ represent a Python
+interpreter executable for a specific Python version X, X.Y & X.Y.Z
+respectively.
+
+Testing environment is generally set up as follows:
+
+1. Install Python.
+2. Install ``setuptools`` (using ``setup_ez.py`` or from the source
+   distribution).
+3. Install ``pip`` using ``setuptools`` (optional).
+4. Install ``pytest`` using ``pip`` or ``setuptools``.
+
+This should hold for all Python releases except some older ones explicitly
+listed below.
+
+To run all of the project unit tests with a specific interpreter without
+additional configuration options run the project's ``setup.py`` script with the
+'test' parameter and an appropriate Python interpreter. E.g. run any of the
+following from the top level project folder::
+
+  py243 setup.py test
+  py27 setup.py test
+  py3 setup.py test
+
+To have more control over the test suite run it from the top level project
+folder using ``pytest``, e.g.
+
+* Using a Python 2.x interpreter::
+
+    py27 -m pytest
+
+* Using a Python 3.x interpreter::
+
+    py33 setup.py build & py33 -m pytest build
+
+This way you can specify additional ``pytest`` options on the command-line.
+
+In both cases, tests run using Python interpreter version 3.x will be run in the
+build folder constructed by the ``setup.py`` script running the ``py2to3`` tool
+on the project's sources. You might need to manually remove the build folder in
+order to have sources in it regenerated when wanting to run the test suite using
+a different Python 3.x interpreter version, as those sources are regenerated
+based solely on the original & processed source file timestamp information and
+not the Python version used to process them.
+
+See the ``pytest`` documentation for a detailed list of available command-line
+options. Some interesting ones:
+
+  -l          show local variable state in tracebacks
+  --tb=short  shorter traceback information for each failure
+  -x          stop on first failure
+
+On Windows you might have a problem setting up multiple parallel Python
+interpreter versions in case they match their major and minor version numbers,
+e.g. Python 2.4.3 & 2.4.4. In those cases, standard Windows installer will
+automatically remove the previous installation instead of simply adding a new
+one. In order to achieve such parallel setup we suggest the following steps:
+
+1. Install the first version in a dummy folder, and do so for the current user
+   only.
+#. Copy the dummy target folder to the desired folder for the first
+   installation, e.g. Python243.
+#. Uninstall the original version.
+#. Set up a shortcut or a batch script (e.g. py243.cmd) for running this
+   interpreter without having to have it added to the system path.
+#. Repeat the steps for the second installation.
+
+Installing Python for the current user only is necessary in order to make Python
+install all of its files into the target folder and not move some of them into
+shared system folders.
+
+Note that this will leave you without start menu or registry entries for these
+Python installations. Registry entries should be needed only if you want to run
+some external Python package installation tool requiring those entries in order
+to determine where to install its package data. In that case you can set those
+entries manually, e.g. by using a script similar to the one found at
+`<http://nedbatchelder.com/blog/201007/installing_python_packages_from_windows_installers_into.html>`_.
+
+Notes on setting up specific Python versions
+--------------------------------------------
+
+Python 2.4.3
+
+* Does not work with HTTPS links so you can not use the Python package index
+  directly, since it, at some point, switched to using HTTPS links only.
+
+  * You could potentially work around this problem by somehow mapping its https:
+    links to http: ones or download its link page manually, locally modify it to
+    contain http: links and then use that download link page instead of the
+    default downloaded one.
+  * An alternative and tested solution is to install into Python 2.4.4 and then
+    copy all the related site-packages entries from that installation into this
+    one.
+
+    * For ``pytest`` 2.4.1 with ``py`` library version 1.4.15 the following data
+      was copied.
+
+      * Folders::
+
+          _pytest
+          argparse-1.2.1-py2.4.egg-info
+          py
+          py-1.4.15-py2.4.egg-info
+          pytest-2.4.1-py2.4.egg-info
+
+      * Files::
+
+          argparse.py
+          pytest.py
+
+Python 2.4.x
+
+* Can not run ``pip`` using ``python.exe -m pip``. Workaround is to use one of
+  the ``pip`` startup scripts found in the Python installation's ``Scripts``
+  folder or to use the following invocation::
+
+    py244 -c "import pip;pip.main()" <regular-pip-options>
+
+* ``pip``
+
+  * 1.1 - last version supporting Python 2.4.
+
+    * Install using::
+
+        py244 -m easy_install pip==1.1
+
+  * Can not be run using ``python.exe -m pip``.
+
+    * Workaround is to use one of the ``pip`` startup scripts found in the
+      Python installations ``Scripts`` folder or the following invocation::
+
+        py244 -c "import pip;pip.main()" <regular-pip-options>
+
+* ``pytest``
+
+  * 2.4.1 - last version supporting Python 2.4.
+
+    * Install using::
+
+        py244 -c "import pip;pip.main()" install pytest==2.4.1
+
+  * Depends on the ``py`` package library version >= 1.4.16. However those
+    versions fail to install with Python 2.4 (tested up to and including
+    1.4.18).
+
+    * May be worked around by forcing ``pytest`` to use an older ``py`` package
+      library version:
+
+      1. Run the ``pytest`` installation using ``pip``. It will fail but it will
+         install everything needed except the ``py`` package library.
+      #. Install the ``py`` package library version 1.4.15 using::
+
+           py244 -c "import pip;pip.main()" install py==1.4.15
+
+    * If worked around by using the ``py`` 1.4.15 library version, ``pytest``'s
+      startup scripts will not work (as they explicitly check ``pytest``'s
+      package dependencies), but ``pytest`` can still be run using::
+
+        py244 -m pytest <regular-pytest-options>
+
+
+STANDARDS CONFORMANCE
+=================================================
+
+There seems to be no complete standards conformance overview for the suds
+project. This section contains just some related notes, taken down while hacking
+on this project. As more related information is uncovered, it should be added
+here as well, and eventually this whole section should be moved to the project's
+user documentation.
+
+Interpreting message parts defined by a WSDL schema
+---------------------------------------------------
+
+* Each message part is interpreted as a single parameter.
+
+  * What we refer to here as a 'parameter' may not necessarily correspond 1-1 to
+    a Python function argument passed when using the suds library's Python
+    function interface for invoking web service operations. In some cases suds
+    may attempt to make the Python function interfaces more intuitive to the
+    user by automatically unwrapping a parameter as defined inside a WSDL schema
+    into multiple Python function arguments.
+
+* In order to achieve interoperability with existing software 'in the wild',
+  suds does not fully conform to the WSDL 1.1 specification with regard as to
+  how message parts are mapped to input data contained in SOAP XML web service
+  operation invocation request documents.
+
+  * WSDL 1.1 standard states:
+
+    * 2.3.1 Message Parts.
+
+      * A message may have message parts referencing either an element or a type
+        defined in the WSDL's XSD schema.
+      * If a message has a message part referencing a type defined in the WSDL's
+        XSD schema, then that must be its only message part.
+
+    * 3.5 soap:body.
+
+      * If using document/literal binding and a message has a message part
+        referencing a type defined in the WSDL's XSD schema then that part
+        becomes the schema type of the enclosing SOAP envelope Body element.
+
+  * Suds supports multiple message parts, each of which may be related either to
+    an element or a type.
+  * Suds uses message parts related to types, as if they were related to an
+    element, using the message part name as the representing XML element name in
+    the constructed related SOAP XML web service operation invocation request
+    document.
+  * WS-I Basic Profile 1.1 standard explicitly avoids the issue by stating the
+    following:
+
+    * R2204 - A document/literal binding in a DESCRIPTION MUST refer, in each of
+      its soapbind:body element(s), only to wsdl:part element(s) that have been
+      defined using the element attribute.
+
+  * Rationale.
+
+    * No other software has been encountered implementing the exact
+      functionality specified in the WSDL 1.1 standard.
+    * Already done in the original suds implementation.
+    * Example software whose implementation matches our own.
+
+      * SoapUI.
+
+        * Tested with version 4.6.1.
+
+      * WSDL analyzer & invoker at `<http://www.validwsdl.com>`_.
+
+WSDL XSD schema interpretation
+------------------------------
+
+* ``minOccurs``/``maxOccurs`` attributes on ``all``, ``choice`` & ``sequence``
+  schema elements are ignored.
+
+  * Rationale.
+
+    * Already done in the original suds implementation.
+
+  * Extra notes.
+
+    * SoapUI (tested with version 4.6.1).
+
+      * For ``all``, ``choice`` & ``sequence`` schema elements with their
+        ``minOccurs`` attribute set to "0", does not explicitly mark elements
+        found in such containers as optional.
+
+* Supports sending multiple same-named web service operation parameters, but
+  only if they are specified next to each other in the constructed web service
+  operation invocation request document.
+
+  * Done by passing a list or tuple of such values to the suds constructed
+    Python function representing the web service operation in question.
+  * Rationale.
+
+    * Already done in the original suds implementation.
+
+  * Extra notes.
+
+    * Such same-named values break other web service related tools as well, e.g.
+      WSDL analyzer & invoker at `<http://www.validwsdl.com>`_.
diff --git a/LICENSE b/LICENSE.txt
similarity index 100%
rename from LICENSE
rename to LICENSE.txt
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..a6a110c
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,13 @@
+#   Additional files to be included in the source distribution package (created
+# by running 'setup.py sdist'). Theoretically we could avoid having to manually
+# maintain this list by using a setuptools plugin that would automatically
+# include all files under Mercurial version control, but the setuptools_hg we
+# tried out did not work correctly with Python 3.
+
+# Top level project files.
+include notes/*.rst
+include notes/*.txt
+include ez_setup.py
+include HACKING.rst
+include LICENSE.txt
+include TODO.txt
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..d4546ee
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,47 @@
+Metadata-Version: 1.1
+Name: suds-jurko
+Version: 0.6
+Summary: Lightweight SOAP client (Jurko's fork)
+Home-page: http://bitbucket.org/jurko/suds
+Author: Jurko Gospodnetic
+Author-email: jurko.gospodnetic at pke.hr
+License: (specified using classifiers)
+Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.6.tar.bz2
+Description: 
+        ---------------------------------------
+        Lightweight SOAP client (Jurko's fork).
+        ---------------------------------------
+        
+          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
+        dot com) hosted at 'http://fedorahosted.org/suds'.
+        
+          'Suds' is a lightweight SOAP-based web service client for Python
+        licensed under LGPL (see the LICENSE.txt file included in the
+        distribution).
+        
+          This is hopefully just a temporary fork of the original suds Python
+        library project created because the original project development seems
+        to have stalled. Should be reintegrated back into the original project
+        if it ever gets revived again.
+        
+        
+Keywords: SOAP,web,service,client
+Platform: (specified using classifiers)
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.4
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.0
+Classifier: Programming Language :: Python :: 3.1
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Topic :: Internet
+Obsoletes: suds
diff --git a/README b/README
deleted file mode 100644
index f207ae5..0000000
--- a/README
+++ /dev/null
@@ -1,578 +0,0 @@
-OVERVIEW
-
-The "Suds" web services client is a lightweight soap-based client for python
-the is licensed under LGPL.
-
-For details, visit:
-
-  * Project site: https://fedorahosted.org/suds/
-  * Documentation: https://fedorahosted.org/suds/wiki/Documentation
-  * Epydocs: http://jortel.fedorapeople.org/suds/doc/
-   
-
-RELEASE-NOTES:
-=================================================
-
-version 0.3.6 (04-31-09):
-  * Change hard coded /tmp/suds to tempfile.gettempdir() and create suds/ on demand.
-  * Fix return type for Any.get_attribute().
-  * Update http caching to ignore file:// urls.
-  * Better logging of messages when only the reply is injected.
-  * Fix XInteger and XFloat types to translate returned arrays properly.
-  * Fix xs:import schema with same namespace.
-  * Update parser to not load external references and add Import.bind() for XMLSchema.xsd location.
-  * Add schema doctor - used to patch XSDs at runtime. (See Option.doctor)
-  * Fix deprecation warnings in python 2.6.
-  * Add behavior for @default defined on <element/>.
-  * Change @xsi:type value to always be qualified for doc/literal. (reverts 0.3.5 change).
-  * Add Option.xstq option to control when @xsi:type is qualified.
-  * Fixed Tickets: #64, #129, #205, #206, #217, #221, #222, #224, #225, #228, #229, #230 
-
-version 0.3.5 (04-16-09):
-  * Adds http caching. Default is (1) day. Does not apply to method invocation. See: documentation for details.
-  * Removed checking fedora version check in spec since no longer building < fc9.
-  * Updated makefile to roll tarball with tar.sh.
-  * Moved bare/wrapped determination to wsdl for document/literal.
-  * Refactored Transport into a package (provides better logging of http headers).
-  * Fixed Tickets: #207, #209, #210, #212, #214, #215 
-
-version 0.3.4 (02-24-09):
-  * Static (automatic) Import.bind('http://schemas.xmlsoap.org/soap/encoding/'),
-      users no longer need to do this.
-  * Basic ws-security with {{{UsernameToken}}} and clear-text password only.
-  * Add support for ''sparse'' soap headers via passing dictionary
-  * Add support for arbitrary user defined soap headers
-  * Fixes service operations with multiple soap header entries.
-  * Schema loading and dereferencing algorithm enhancements.
-  * Nested soap multirefs fixed.
-  * Better (true) support for elementFormDefault="unqualified" provides more 
-      accurate namespaing.
-  * WSDL part types no longer default to WSDL targetNamespace.
-  * Fixed Tickets: #4, #6, #21, #32, #62, #66, #71, #72, #114, #155, #201.
-
-version 0.3.3 (11-31-08):
-  * No longer installs (tests) package.
-  * Implements API-3 proposal ( https://fedorahosted.org/suds/wiki/Api3Proposal )
-     - Pluggable transport
-    - Keyword method arguments
-    - Baisc http authentication in default transport
-  * Add namespace prefix normalization in soap message.
-  * Better soap message pruning of empty nodes.
-  * Fixed Tickets: #51 - #60
-
-  
-version 0.3.2 (11-7-08):
-  * SOAP {{{MultiRef}}} support ''(1st pass added r300)''
-  * Add support for new schema tags:
-     * <xs:include/>
-     * <xs:simpleContent/>
-     * <xs:group/>
-     * <xs:attributeGroup/>
-  * Added support for new xs <--> python type conversions:
-     * xs:int
-     * xs:long
-     * xs:float
-     * xs:double
-  * Revise marshaller and binding to further sharpen the namespacing of nodes produced.
-  * Infinite recursion fixed in ''xsd'' package dereference() during schema loading.
-  * Add support for <wsdl:import/> of schema files into the wsdl root <definitions/>.
-  * Fix double encoding of (&)
-  * Add Client API:
-     * setheaders() - same as keyword but works for all invocations.
-     * addprefix() - mapping of namespace prefixes.
-    * setlocation() - Override the location in the wsdl; same as keyword except for all calls.
-    * setproxy() - same as proxy keyword but for all invocations.
-  * Add proper namespace prefix for soap headers.
-  * Fixed Tickets: #5, #12, #34, #37, #40, #44, #45, #46, #48, #49, #50, #51
-
-  
-version 0.3.1 (10-1-08):
-
- * Quick follow up to the 0.3 release that made working multi-port service definitions
-    harder then necessary.  After consideration (and a good night sleep),
-    it seemed obvious that a few changes would make this much easier: 1) filter out 
-    the non-soap bindings - they were causing the real trouble;  2) since most servers
-    are happy with any of the soap bindings (soap 1.1 and 1.2), ambigious references to methods 
-    when invoking then without the port qualification will work just fine in almost every
-    case.  So, why not just allow suds to select the port.  Let's not make the user do it 
-    when it's not necessary.  In most cases, uses on 0.2.9 and earlier will not have to
-    update there code when moving to 0.3.1 as they might have in 0.3.
-
-    
-version 0.3 (9-30-08):
-
-  * Extends the support for multi-port services introduced in 0.2.9.  This addition,
-     provides for multiple services to define the *same* method and suds will
-     handle it properly.  See section 'SERVICES WITH MULTIPLE PORTS:'
-     
-  * Add support for multi-document document/literal soap binding style.
-     See section 'MULTI-DOCUMENT Docuemnt/Literal:'
-  
-  * Add support for (xs:group, xs:attributeGroup) tags.
-  
-  * Add Client.last_sent() and Client.last_received().
-    
-version 0.2.9 (9-09-08):
-   
-   * Support for multiple ports within a service.
-   * Attribute references <xs:attribute ref=""/>
-   * Make XML special character encoder in sax package - pluggable 
-
-
-version 0.2.8 (8-28-08):
-
-   * Update document/literal binding to always send the document root referenced by the <part/>.
-      After yet another review of the space and user input, seems like the referenced 
-      element is ALWAYS the document root.
-
-   * Add support for 'binding' schemaLocations to namespace-uri.  
-     This is for imports that don's specify a schemaLocation and still expect the schema 
-     to be downloaded.  Eg: Axis references 'http://schemas.xmlsoap.org/soap/encoding/' 
-     without a schemaLocation.  So, by doing this:
-        >
-        > from suds.xsd.sxbasic import Import.
-        > Import.bind('http://schemas.xmlsoap.org/soap/encoding/')
-        >
-     The schema is bound to a schemaLocation and it is downloaded.
-
-   * Basic unmarshaller doesn't need a /schema/. 
-      Should have been removed during refactoring but was missed.
-
-   * Update client to pass kwargs to send() and add /location/ kwarg for overriding the
-      service location in the wsdl.
-
-   * Update marshaller to NOT emit XML for object attributes that represent elements and/or attributes that
-      are *both* optional and value=None.
-         * Update factory (builder) to include all attributes.
-         * Add optional() method to SchemaObject.
-
-   * Update wsdl to override namespace in operation if specified.
-   
-   * Fix schema loading issue - build all schemas before processing imports.
-   
-   * Update packaging in preparation of submission to fedora
-
-      
-version 0.2.7 (8-11-08):
-
-   * Add detection/support for document/literal - wrapped and unwrapped.
-   * Update document/literal {wrapped} to set document root (under <body/>) to be the
-      wrapper element (w/ proper namespace).
-   * Add support for <sequence/>, <all/> and <choice/> having maxOccurs and have the
-      which causes the unmarshaller to set values for elements contained in an unbounded
-      collection as a list.
-   * Update client.factory (builder) to omit children of <choice/> since the 'user' really needs
-      to decide which children to include.
-   * Update flattening algorithm to prevent re-flattening of types from imported schemas.
-   * Adjustments to flattening/merging algorithms.
-   
-
-version 0.2.6 (8-5-08):
-
-   * Fix ENUMs broken during xsd package overhaul.
-   * Fix type as defined in ticket #24.
-   * Fix duplicate param names in method signatures as reported in ticket #30.
-   * Suds licensed as LGPL.
-   * Remove logging setup in suds.__init__() as suggested by patch in ticket #31.  Users will
-      now need to configure the logger.
-   * Add support for Client.Factory.create() alt: syntax for fully qualifying the type to be
-      built as: {namespace}name.  Eg: client.factory.create('{http://blabla.com/ns}Person')
-
-
-version 0.2.5 (8-01-08):
-
-   * Overhauled the (XSD) package.  This new (merging) approach is simpler and should be
-      more reliable and maintainable.  Also, should provide better performance since the merged
-      schema performes lookups via dictionary lookup.
-      
-      This overhaul should fix current TypeNotFound and <xs:extension/> problems, I hope :-).
-      
-   * Fixed dateTime printing bug.
-
-   * Added infinite recursion prevention in builder.Builder for xsd types that contain themselves.
-
-
-version 0.2.4 (7-28-08):
-
-   * Added support for WSDL imports: <wsdl:import/>
-   * Added support for xsd<->python type conversions (thanks: Nathan Van Gheem) for:
-      * xs:date
-      * xs:time
-      * xs:dateTime
-   * Fixed:
-      * Bug: Schema <import/> with schemaLocation specified.
-      * Bug: Namespaces specified in service description not valid until client/proxy is printed.
-
-
-version 0.2.3 (7-23-08):
-
-   * Optimizations.
-
-
-version 0.2.2 (7-8-08):
-
-   * Update exceptions to be more /standard/ python by using Exception.__init__() to set Exception.message as
-     suggested by Ticket #14; update bindings to raise WebFault passing (p)
-   
-   * Add capability in bindings to handle multiple root nodes in the returned values;
-     returned as a composite object unlike when lists are returned
-   
-   * Fix soapAction to be enclosed by quotes
-   
-   * Add support for <xs:all/>
-   
-   * Fix unbounded() method in SchemaObject
-   
-   * Refactored schema into new (xsd) package.  Files just getting too big. Added execute() to
-     Query and retrofitted suds to execute() query instead of using Schema.find() directly.  
-     Also, move hokey start() methods from schema, as well as, query incrementation.
-   
-   * Add inject keyword used to inject outbound soap messages and/or inbound reply messages.
-      Refactor SoapClient and 
-        1) rename send() to invoke()
-        2) split message sending from invoke() and place in send();
-      Add TestClient which allows for invocation kwargs to have inject={'msg=, and reply='} for message
-      and reply injection
-     
-   * Add Namespace class to sax for better management of namespace behavior;
-     retrofix suds to import and use Namespace
-   
-   * Change the default namespace used to resolve referenced types (having attribues @base="", at type="")
-      so that when no prefix is specified: uses XML (node) namesapce instead of the targetNamespace.
-   
-   * Apply fix as defined by davidglick at onenw.org in ticket #13
-   
-   * Update service definition to print to display service methods as ' my_method(xs:int arg0, Person arg1) '
-      instead of ' my_method(arg0{xs:int}, arg1{Person}) ' which is more like traditional method signatures
-   
-   * Add xsd/python type converstion to unmarshaller (XBoolean only); refactor unmarshaller to use Content
-      class which makes apis cleaner, adds symmetry between marshaller(s) and unmarshaller(s),
-      provides good mechanism for schema-property based type conversions
-   
-   * Refactor marshaller with Appenders; add nobuiltin flag to resolve() to support fix for
-      returned_type() and returnes_collection() in bindings.
-   
-   * Add support for (202,204) http codes
-   
-   * Add XBoolean and mappings; add findattr() to TreeResolver in preparation for type conversions
-   
-   * Updated schema and schema property loading (deep recusion stopped); Changed Imported schemas so then no
-      longer copy imported schemas, rather the import proxies find requests; Add ServiceDefinition class which
-      provides better service inspection; also provides namespace mapping and show types; schema property api simplified;
-      support for xs:any and xs:anyType added; Some schema lookup problems fixed; Binding classes refactored slightly;
-      A lot of debug logging added (might have to comment some out for performance - some of the args are expensive).
-   
-   * Add sudsobject.Property; a property is a special Object that contains a (value) attributeand is returned by the
-      Builder (factory) for schema-types without children such as: <element/> and <simpleType/>; Builder, Marshallers
-      and Resolvers updated to handle Properties; Resolver, Schema also updated to handle attribute lookups (this was missing)
-   
-   * Add groundwork for user defined soap headers.
-   
-   * Fix elementFormDefault per ticket #7
-   
-   * Remove unused kwargs from bindings; cache bindings in wsdl; retrofit legacy ServiceProxy to delegate to {new} Client API;
-      remove keyword nil_supported in favor of natural handling by 'nillable' attribute on <element/> within schemas
-   
-   * Add support for <element/> attribute flags (nillable and form)
-   
-   * Add the Proxy (2nd generation API) class
-   
-   * Add accessor/conversion functions to that user don't need to access __x__ attributes.
-      Also add todict() and get_items() for easy conversion to dictionary and iteration
-   
-   * Search top-level elements for @ref before looking deeper
-   
-   * Add derived() to SchemaObject.  This is needed to ensure that all derived types (wsdl classes)
-      are qualified by xsi:type without specifying the xsi:type for all custom types as did in earlier
-      releases of suds.  Update the literal marshaller to only add the xsi:type when the type needs
-      to be specified.
-   
-   * Change ns promotion in sax to prevent ns promoted to parent when parent has the prefix.
-   
-   * Changed binding returned_type() to return the (unresolved) Element 
-   
-   * In order to support the new features and fix reported bugs,
-      I'm in the process of refactoring and hopefully evolving the components
-      in Suds that provide the input/output translations:
-   
-     * Builder ( translates: XSD objects => python objects )
-     * Marshaller ( translates: python objects => XML/SOAP )
-     * Unmarshaller ( translates: soap/xml => python objects )
-   
-     This evolution will provide better symmetry between these components as follows:
-   
-     The Builder and Unmarshaller will produce python
-     (subclass of sudsobject.Object) objects with:
-   
-     *  __metadata__.__type__ = XSD type (SchemaObject)
-     * subclass name ( __class__.__name__ ) = schema-type name.
-   
-     and
-   
-     The Marshaller(s), while consuming python objects produced by the Builder or
-     Unmarshaller, will leverage this standard information to
-     produce the appropriate output ( XML/SOAP ).
-   
-     The 0.2.1 code behaves *mostly* like this but ... not quite.
-     Also, the implementations have some redundancy.
-   
-     While doing this, it made sense to factor out the common schema-type "lookup"
-     functionality used by the Builder, Marshallers and Unmarshaller classes into a
-     hierarchy of "Resolver" classes.  This reduces the complexity and redundancy
-     of the Builder, Marshallers and Unmarshaller classes and allows for better
-     modularity.  Once this refactoring was complete, the difference between the
-     literal/encoded Marshallers became very small.  Given that the amount of code
-     in the bindings.literal and bindings.encoded packages was small (and getting smaller)
-     and in the interest of keeping the Suds code base compact, I moved all of the
-     marshalling classes to the bindings.marshaller module.
-     All of the bindings.XX sub-packages will be removed.
-   
-     The net effect:
-   
-     All of the Suds major components:
-   
-     * client (old: service proxy)
-     * wsdl
-       * schema (xsd package)
-       * resolvers
-     * output (marshalling)
-     * builder
-     * input (unmarshalling)
-   
-     Now have better:
-   
-      * modularity
-      * symmetry with regard to Object metadata.
-      * code re-use (< 1% code duplication --- i hope)
-      * looser coupling
-   
-     ... and better provide for the following features/bug-fixes:
-   
-     * (fix) Proper level of XML element qualification based on
-        <schema elementFormDefault=""/> attribute.  This will ensure that when
-        elementFormDefault="qualified", Suds will include the proper namespace on
-        root elements for both literal and encoded bindings.  In order for this to
-        work properly, the literal marshaller (like the encoded marshaller) needed
-        to be schema-type aware.  Had i added the same schema-type lookup as the
-        encoded marshaller instead of the refactoring described above, the two
-        classes would have been almost a complete duplicate of each other :-(
-   
-   * The builder and unmarshaller used the schema.Schema.find()
-     to resolve schema-types.  They constructed a path as "person.name.first"
-     to resolve types in proper context.  Since the Schema.find() was stateless,
-     it resolved the intermediate path elements on every call.  The new resolver
-     classes are statefull and resolve child types *much* more efficiently.
-   
-   * Prevent name collisions in sudsobject.Object like the items() method.  I've moved all
-     methods (including class methods) to a Factory class that is included in the Object class
-     as a class attr ( __factory__ ).  Now that *all* attributes have python built-in naming,
-     we should not have any more name collisions.  This of course assumes that no wsdl/schema
-     entity names will have a name with the python built-in naming convention
-     but I have to draw the line somewhere :-)
-     
-
-version 0.2.1 (5-8-08):
-
-   * Update the schema.py SchemaProperty loading sequence so that the schema is loaded in 3 steps: 
-         1) build the raw tree. 
-         2) resolve dependancies such as @ref and @base.  
-         3) promote grandchildren as needed to flatten (denormalize) the tree.
-         
-       The wsdl was also changed to only load the schema once and store it.  The schema collection was 
-       changed to load schemas in 2 steps:
-         1) create all raw schema objects.
-         2) load schemas.  
-       
-       This ensure that local <import/>'d schemas can be found when referenced out of order.  
-       The sax.py Element interface changed: attribute() replaced by get() and set().  
-       Also, __getitem__ and __setitem__ can be used to access attribute values.  
-       Epydocs updated for sax.py.  And ... last <element ref=/> now supported properly.
-
-   * fix logging by: NOT setting to info in suds.__init__.logger(); set handler on root logger 
-      only; moved logger (log) from classes to modules and use __name__ for logger name.
-      NOTE: This means that to enable soap message logging:
-           >
-           > logger('suds.serviceproxy').setLevel(logging.DEBUG)
-           > 
-         -- instead of --
-           >
-           > logger('serviceproxy').setLevel(logging.DEBUG)
-           >
-      
-   * Add support for (xsd) schema <attribute/> nodes which primarily affects objects returned by the Builder
-   
-   * Update serviceproxy.py:set_proxies() to log DEBUG instead of INFO.
-   
-   * Enhance schema __str__ to show both the raw xml and the model (mostly for debugging).
-
-
-version-0.2 (04-28-08):
-
-  * Contains the first cut at the rpc/encoded soap style.
- 
-  * Replaced Property class with suds.sudsobject.Object.  The Property class was developed a long time
-     ago with a slightly different purpose.  The suds Object is a simpler (more straight forward) approach that 
-     requires less code and works better in the debugger. 
-
-  * The Binding (and the encoding) is selected on a per-method basis which is more consistent with the wsdl.
-     In <= 0.1.7, the binding was selected when the ServiceProxy was constructed and used for all service
-     methods.  The binding was stored as self.binding.  Since the WSDL provides for a separate binding style and
-     encoding for each operation, Suds needed to be change to work the same way.
- 
-  * The (nil_supported) and (faults) flag(s) passed into the service proxy using **kwargs.  In addition to these
-     flags, a (http_proxy) flag has been added and is passed to the urllib2.Request object.  The following args
-     are supported:
-       * faults = Raise faults raised by server (default:True), else return tuple from service method invocation
-                        as (http code, object).
-       * nil_supported = The bindings will set the xsi:nil="true" on nodes that have a value=None when this
-                                      flag is True (default:True).  Otherwise, an empty node <x/> is sent.
-       * proxy = An http proxy to be specified on requests (default:{}).
-                       The proxy is defined as {protocol:proxy,}
-                                      
-  * Http proxy supported (see above).
-  
-  * ServiceProxy refactored to delegate to a SoapClient.  Since the service proxy exposes web services via getattr(),
-     any attribute (including methods) provided by the ServiceProxy class hides WS operations defined by the
-     wsdl.  So, by moving everything to the SoapClient, wsdl operations are no longer hidden without
-     having to use *hoky* names for attributes and methods in the service proxy.  Instead, the service proxy has
-     __client__ and __factory__ attributes (which really should be at low risk for name collision).  For now the 
-     get_instance() and get_enum() methods have not been moved to preserve backward compatibility.  Although,
-     the prefered API change would to replace:
-     
-     > service = ServiceProxy('myurl')
-     > person = service.get_instance('person')
-     
-     ... with something like ...
-     
-     > service = ServiceProxy('myurl')
-     > person = service.__factory__.get_instance('person')
-     
-     After a few releases giving time for users to switch the new API, the get_instance() and get_enum()
-     methods may be removed with a notice in big letters.
-     
-   * Fixed problem where a wsdl doesn't define a <schema/> section and Suds can't resolve the prefixes for the
-      http://www.w3.org/2001/XMLSchema namespace to detect builtin types such as (xs:string).
-      
-      
-version-0.1.7 (04-08-08):
-
-  * Added Binding.nil_supported to controls how property values (out) = None and empty tag (in) are processed.
-    * service.binding.nil_supported = True -- means that property values = None are marshalled (out) as
-      <x xsi:nil=true/> and <x/> is unmarshalled as '' and <x xsi:nil/> is unmarshalled as None.
-    * service.binding.nil_supported = False -- means that property values = None are marshalled (out) as
-      <x/> and <x/> *and* <x xsi:nil=true/> is unmarshalled as None.
-      The xsi:nil is really ignored.
-    * THE DEFAULT IS (TRUE)
-
-  * Sax handler updated to handle multiple character() callbacks when the sax parser "chunks" the text.
-    When the node.text is None, the node.text is set to the characters.  Else, the characters are appended.
-    Thanks - andrea.spinelli at imteam.it
-  
-  * Replaced special (text) attribute with __text__ to allow for natural elements named "text"
-  
-  * Add unicode support by:
-    * Add __unicode__ to all classes with __str__
-    * Replace all str() calls with unicode().
-    * __str__() returns UTF-8 encoded result of __unicode__.
-
-  * XML output encoded as UTF-8 which matches the HTTP header and supports unicode.
-  
-  * SchemaCollection changed to provide the builtin() and custom() methods.  To support this, findPrefixes() was added to the
-    Element in sax.py.  This is a better approach anyway since the wsdl and schemas may have many prefixes
-    to http://www.w3.org/2001/XMLSchema.  Tested with both doc/lit and rpc/lit bindings
-  
-  * Refactored bindings packages from document & rpc to literal & encoded
-  
-  * Contains the completion of *full* namespace support as follows:
-  
-    * Namespace prefixes are no longer stripped from attribute values that
-      reference types defined in the wsdl.
-    * Schema's imported using <import/> should properly handle namespace and prefix
-      mapping and re-mapping as needed.
-    * All types are resolved, using fully qualified (w/ namespaces) lookups.
-    * Schema.get_type() supports paths with and without ns prefixes.  When no prefix
-      is specified the type is matched using the schema's target namespace.
-  
-  * Property maintains attribute names (keys) in the order added. This also means
-    that get_item() and get_names() return ordered values.
-     ( Although, I suspect ordering really needs to be done in the marshaller using the
-        order specified in the wsdl/schema )
-  
-  Major refactoring of the schema.py. The primary goals is perparation for type lookups that are
-  fully qualified by namespace.  Once completed, the prefixes on attribute values will not longer
-  be stripped (purged).
-  Change Summary:
-    1) SchemaProperty overlay classes created at __init__ instead of on-demand.
-    2) schema imports performed by new Import class instead of by Schema.
-    3) Schema loads top level properties using a factory.
-    4) All SchemaProperty /children/ lists are sorted by __cmp__ in SchemaProperty derived classes.
-       This ensures that types with the same name are resolved in the following order (Import, Complex, Simple, Element).
-    5) All /children/ SchemaProperty lists are constructed at __init__ instead of on-demand.
-    6) The SchemaGroup created and WSDL class updated.  This works better then having the wsdl aggregate the <schema/>
-       nodes which severs linkage to the wsdl parent element that have namespace prefix mapping.
-    7) <import/> element handles properly in that both namespace remapping and prefix re-mapping of the imported schema's
-       targetNamespace and associated prefix mapping - is performed.
-         Eg: SCHMEA-A has prefix (tns) mapped as xmlns:tns=http://nsA and has targetNamespace=http://nsA.
-         SCHEMA-B is importing schema A and has prefix (abc) mapped as xmlns:abc=http://nsABC.
-         SCHEMA-B imports A as <import namespace=http://nsB xxx schemaLocation=http://nsA/schema-a.xsd>.
-         So, since SCHEMA-B will be referencing elements of SCHEMA-A with prefix (abc) such as abc:something, SCHEMA-A's
-         targetNamespace must be updated as http://nsABC and all element with type=tns:something must be updated to be
-         type=abc:something so then can be resolved.
-
-  * Fixes unmarshalling problem where nodes are added to property as (text, value).  This as introduced when the
-    bindings were refactored.
-
-  * Fixed various Property print problems.
-
-  Notes:
-
-    Thanks to Jesper Noehr of Coniuro for the majority of the rpc/literal binding and
-    for lots of collaboration on #suds.
-    
-    
-version-0.1.6 (03-06-08):
-
- * Provides proper handling of wsdls that contain schema sections containing
-    xsd schema imports: <import namespace="" schemaLocation=""?>.  The
-    referenced schemas are imported when a schemaLocation is specified.
-* Raises exceptions for http status codes not already handled.
-
-
-version-0.1.5( 02-21-08 ):
-
- * Provides better logging in the modules get logger by hierarchal names.
- * Refactored as needed to truely support other bindings.
- * Add sax module which replaces ElementTree.  This is faster, simpler and
-   handles namespaces (prefixes) properly.
-   
-   
-version-0.1.4 (12-21-07):
-
-  * Provides for service method parameters to be None.
-  * Add proper handling of method params that are lists of property
-    objects.
-    
-    
-version-0.1.3 (12-19-07):
-
-  * Fixes problem where nodes marked as a collection (maxOccurs > 1) not
-     creating property objects with value=[] when mapped-in with < 2
-     values by the DocumentReader.  Caused by missing the 
-     bindings.Document.ReplyHint.stripns() (which uses the DocumentReader.stripns())
-     conversion to DocumentReader.stripn() now returning a tuple (ns,tag) as
-     of 0.1.2.
-
-
-version-0.1.2 (12-18-07):
-
-  This release contains an update to property adds:
-    * metadata support
-    * overrides: __getitem__, __setitem__, __contans__
-    * changes property(reader|writer) to use the property.metadata
-       to handle namespaces for XML documents.
-    * fixes setup.py requires.
-    
-    
-version-0.1.1 (12-17-07):
-
-  This release marks the first release in fedora hosted.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b506cbf
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,1236 @@
+Overview
+=================================================
+
+"Suds" is a lightweight SOAP-based web service client for Python licensed under
+LGPL (see the ``LICENSE.txt`` file included in the distribution).
+
+This is hopefully just a temporary fork of the original ``suds`` Python library
+project created because the original project development seems to have stalled.
+Should be reintegrated back into the original project if it ever gets revived
+again.
+
+**Forked project information**
+
+* Project site - http://bitbucket.org/jurko/suds
+* Epydocs documentation - needs to be built from sources
+* Official releases can be downloaded from:
+
+  * BitBucket - http://bitbucket.org/jurko/suds/downloads
+  * PyPI - http://pypi.python.org/pypi/suds-jurko
+
+**Original suds Python library development project information**
+
+* Project site - http://fedorahosted.org/suds
+* Documentation - http://fedorahosted.org/suds/wiki/Documentation
+* Epydocs - http://jortel.fedorapeople.org/suds/doc
+
+For development notes see the ``HACKING.rst`` document included in the
+distribution.
+
+
+Installation
+=================================================
+
+Standard Python installation.
+
+Here are the basic instructions for 3 different installation methods:
+
+#. Using ``pip``
+
+   * Have the ``pip`` package installed.
+   * Run ``pip install suds-jurko``.
+
+#. Using ``easy-install``
+
+   * Have the ``setuptools`` package installed.
+   * Run ``easy_install suds-jurko``.
+
+#. From sources
+
+   * Unpack the source package somewhere.
+   * Run ``python setup.py install`` from the source distribution's top level
+     folder.
+
+Installation troubleshooting:
+-----------------------------
+
+* If automated ``setuptools`` Python package installation fails (used in
+  releases ``0.4.1 jurko 5`` and later), e.g. due to PyPI web site not being
+  available, user might need to install it manually and then rerun the
+  installation.
+* Releases prior to ``0.4.1. jurko 5`` will fail if the ``distribute`` Python
+  package is not already installed on the system.
+* Python 2.4.3 on Windows has problems using automated ``setuptools`` Python
+  package downloads via the HTTPS protocol, and therefore does not work
+  correctly with PyPI which uses HTTPS links to all of its packages. The same
+  does not occur when using Python version 2.4.4.
+
+
+Release notes
+=================================================
+
+version 0.6 (2014-01-24)
+-------------------------
+
+* Based on revision 712 (1e48fd79a1fc323006826439e469ba7b3d2b5a68) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Tested in the following environments:
+
+    * Python 2.4.3/x86, on Windows 7/SP1/x64.
+    * Python 2.4.4/x86, on Windows 7/SP1/x64.
+    * Python 2.7.6/x64, on Windows 7/SP1/x64.
+    * Python 3.2.5/x64, on Windows 7/SP1/x64.
+    * Python 3.3.3/x86, on Windows 7/SP1/x64.
+    * Python 3.3.3/x64, on Windows 7/SP1/x64.
+
+* Fixed sending HTTP request containing non-ASCII unicode data using Python 2.7.
+
+  * Many thanks to mduggan1 and Alexey Sveshnikov for reporting the issue and
+    suggesting patches.
+
+* Fixed unicode data logging issue (contributed by Bouke Haarsma).
+* ``suds.transport.Request`` object string representation cleaned up a bit -
+  added a missing space before the URL to make it consistent with how all the
+  other Request & Reply data is represented in such strings.
+* Fixed issue with ``suds`` client failing to be create its default cache object
+  (e.g. because a folder it needs is write protected) and thus preventing the
+  client from being created without any chance for the user to specify an
+  alternative cache.
+
+  * The default client cache is now instantiated only if user does not
+    explicitly specify some other alternate cache (or even None to disable the
+    whole data caching system).
+  * Many thanks to Arthur Clune for reporting the issue.
+
+* Added explicit tests for URL parameters being passed as unicode or single-byte
+  strings under Python 2 but only unicode strings under Python 3, and improved
+  how such invalid parameter values are reported.
+
+  * This behaviour matches urllib implementation differences between Python 3
+    and earlier Python interpreter versions.
+  * Many thanks to Mesut Tasci for reporting a related issue and preparing the
+    initial patch for it.
+
+* Extra arguments used when making a web service operation call are now reported
+  similar to how this is done for regular Python functions.
+
+  * The extra argument error reporting may be disabled using the new
+    ``extraArgumentErrors`` ``suds`` option.
+  * Basic idea and the initial implementation for this feature contributed by
+    Bouke Haarsma.
+
+* Corrected a typo in the ``BuildError`` exception message.
+* Removed partial support for pre-2.4 Python versions since such old Python
+  versions are no longer officially supported nor are they tested anywhere.
+* Updated documented project links to use HTTP instead of HTTPS protocol.
+* Setup improvements.
+
+  * Fixed setup to work with soft links in the current working folder path
+    (contributed by ryanpetrello).
+  * Project now installed as a zipped egg folder.
+  * No longer attempts to work around Python 2.4.3 issues with urllib HTTPS
+    downloads since now PyPI updated all of its links to HTTPS and the patch
+    would need to become much more complex to deal with this, while making the
+    setup much more difficult to understand and maintain.
+
+    * On the other hand, this is now an extremely old Python version, so the
+      change is not expected to have much impact. Anyone still using this
+      version will just have to work around the issue manually, e.g. by
+      downloading the necessary packages and running their setup procedures
+      directly.
+
+  * ``long_description`` field content wrapped to 72 characters, since
+    ``PKG-INFO`` package distribution metadata file stores this text with an 8
+    space indentation.
+
+* Improved internal project development documentation.
+
+  * ``HACKING.txt`` updated, converted to .rst format & renamed to
+    ``HACKING.rst``.
+  * Started internal project design, research & development notes documentation.
+    Stored in a new ``notes/`` subfolder, included in the project's source
+    distribution, but not its builds or installations.
+
+* Internal test suite improvements.
+
+  * Added unit tests for transport related ``Request`` & ``Reply`` classes.
+  * Improved ``HTTPTransport`` related unit tests.
+  * Split up some web service operation invocation request construction tests
+    into:
+
+    * parameter definition tests
+    * argument parsing tests
+    * binding specific request construction tests
+
+  * Many new tests added & existing ones extended.
+  * Several redundant tests removed.
+  * Added a basic development script for running the project's full test suite
+    using multiple Python interpreter versions under Windows.
+  * Better test support when running with disabled assertion optimizations
+    enabled.
+  * Cleaned up support for running test scripts directly as Python scripts.
+
+    * May now be passed pytest command-line options.
+    * Now return an exit code indicating the test result (0=success,
+      !0=failure).
+
+version 0.5 (2013-11-25)
+------------------------
+
+* Based on revision 712 (1e48fd79a1fc323006826439e469ba7b3d2b5a68) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Tested in the following environments:
+
+    * Python 2.4.3/x86, on Windows 7/SP1/x64.
+    * Python 2.4.4/x86, on Windows 7/SP1/x64.
+    * Python 2.7.6/x64, on Windows 7/SP1/x64.
+    * Python 3.2.5/x64, on Windows 7/SP1/x64.
+    * Python 3.3.3/x86, on Windows 7/SP1/x64.
+    * Python 3.3.3/x64, on Windows 7/SP1/x64.
+
+* Updated the project's versioning scheme and detached it from the original
+  ``suds`` project. The original project's stall seems to be long-term (likely
+  permanent) and making our version information match the original one was
+  getting to be too much of a hassle.
+
+  * For example, with our original versioning scheme, latest pip versions
+    recognize our package releases as 'development builds' and refuse to install
+    them by default (supply the ``--pre`` command-line option to force the
+    install anyway).
+
+* Improved the ``suds`` date/time handling (contributed by MDuggan1, based on a
+  patch attached to issue `#353 <http://fedorahosted.org/suds/ticket/353>`_ on
+  the original ``suds`` project issue tracker).
+
+  * Replaces the timezone handling related fix made in the previous release.
+  * More detailed testing.
+  * Corrected subsecond to microsecond conversion, including rounding.
+  * ``DateTime`` class no longer derived from ``Date`` & ``Time`` classes.
+  * Recognizes more date/time strings valid 'by intuition'.
+  * Rejects more invalid date/time strings.
+
+    * Time zone specifiers containing hours and minutes but without a colon are
+      rejected to avoid confusion, e.g. whether ``+121`` should be interpreted
+      as ``+12:01`` or ``+01:21``.
+    * Time zone specifiers limited to under 24 hours. Without this Python's
+      timezone UTC offset calculation would raise an exception on some
+      operations, e.g. timezone aware ``datetime.datetime``/``time``
+      comparisons.
+
+* Removed several project files related to the original developer's development
+  environment.
+* Removed several internal Mercurial version control system related files from
+  the project's source distribution package.
+* Better documented the project's development & testing environment.
+
+version 0.4.1 jurko 5 (2013-11-11)
+----------------------------------
+
+* Based on revision 712 (1e48fd79a1fc323006826439e469ba7b3d2b5a68) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Tested in the following environments:
+
+    * Python 2.4.3/x86, on Windows 7/SP1/x64.
+    * Python 2.4.4/x86, on Windows 7/SP1/x64.
+    * Python 2.7.3/x64, on Windows 7/SP1/x64.
+    * Python 3.2.3/x64, on Windows 7/SP1/x64.
+    * Python 3.3.2/x86, on Windows 7/SP1/x64.
+    * Python 3.3.2/x64, on Windows 7/SP1/x64.
+
+* Improved Python 3 support.
+
+  * Cache files now used again.
+
+    * Problems caused by cache files being stored in text mode, but attempting
+      to write a bytes object in them. Too eager error handling was then causing
+      all such cached file usage to fail silently.
+
+  * ``WebFault`` containing non-ASCII data now gets constructed correctly.
+  * Fixed issue with encoding of authentication in ``transport/http.py``
+    (contributed by Phillip Alday).
+  * Unicode/byte string handling fixes.
+
+* Fixed encoding long user credentials for basic HTTP authentication in
+  ``transport/http.py`` (contributed by Jan-Wijbrand Kolman).
+* Fixed an ``IndexError`` occurring when calling a web service operation with
+  only a single input parameter.
+* Fixed a log formatting error, originated in the original ``suds`` (contributed
+  by Guy Rozendorn).
+* Fixed local timezone detection code (contributed by Tim Savage).
+* Setup updated.
+
+  * Fixed a problem with running the project setup on non-Windows platforms.
+
+    * ``version.py`` file loading no longer sensitive to the line-ending type
+      used in that file.
+    * Stopped using the ``distribute`` setup package since it has been merged
+      back into the original ``setuptools`` project. Now using ``setuptools``
+      version 0.7.2 or later.
+    * Automatically downloads & installs an appropriate ``setuptools`` package
+      version if needed.
+
+  * ``distutils`` ``obsoletes`` setup parameter usage removed when run using
+    this Python versions earlier than 2.5 as that is the first version
+    implementing support for this parameter.
+
+* Removed different programming techniques & calls breaking compatibility with
+  Python 2.4.
+
+  * String ``format()`` method.
+  * Ternary if operator.
+
+* Project ``README`` file converted to .rst format (contributed by Phillip
+  Alday).
+* Corrected internal input/output binding usage. Output binding was being used
+  in several places where the input one was expected.
+* HTTP status code 200 XML replies containing a ``Fault`` element now
+  consistently as a SOAP fault (plus a warning about the non-standard HTTP
+  status code) both when reporting such faults using exceptions or by returning
+  a (status, reason) tuple.
+
+  * Before this was done only when reporting them using exceptions.
+
+* Reply XML processing now checks the namespace used for ``Envelope`` & ``Body``
+  elements.
+* SOAP fault processing now checks the namespaces used for all relevant tags.
+* Plugins now get a chance to process ``received()`` & ``parsed()`` calls for
+  both success & error replies.
+* SOAP fault reports with invalid Fault structure no longer cause ``suds`` code
+  to break with an 'invalid attribute' exception.
+* SOAP fault reports with no ``<detail>`` tag (optional) no longer cause
+  ``suds`` code to break with an 'invalid attribute' exception when run with the
+  ``suds`` ``faults`` option set to ``False``.
+* Added correct handling for HTTP errors containing no input file information.
+  Previously such cases caused ``suds`` to break with an 'invalid attribute'
+  exception.
+* ``SimClient`` injection keywords reorganized:
+
+  * ``msg`` - request message.
+  * ``reply`` - reply message ('msg' must not be set).
+  * ``status`` - HTTP status code accompanying the 'reply' message.
+  * ``description`` - description string accompanying the 'reply' message.
+
+* Added ``unwrap`` option, allowing the user to disable ``suds`` library's
+  automated simple document interface unwrapping (contributed by Juraj Ivančić).
+* Fixed a problem with ``suds`` constructing parameter XML elements in its SOAP
+  requests in incorrect namespaces in case they have been defined by XSD schema
+  elements referencing XSD schema elements with a different target namespace.
+* ``DocumentStore`` instance updated.
+
+  * Separate ``DocumentStore`` instances now hold separate data with every
+    instance holding all the hardcoded ``suds`` library XML document data.
+  * ``DocumentStore`` now supports a dict-like ``update()`` method for adding
+    new documents to it.
+  * ``Client`` instances may now be given a specific ``DocumentStore`` instance
+    using the 'documentStore' option. Not specifying the option uses a shared
+    singleton instance. Specifying the option as ``None`` avoids using any
+    document store whatsoever.
+  * Suds tests no longer have to modify the global shared ``DocumentStore`` data
+    in order to avoid loading its known data from external files and so may no
+    longer affect each other by leaving behind data in that global shared
+    ``DocumentStore``.
+  * Documents may now be fetched from a ``DocumentStore`` using a transport
+    protocol other than ``suds``. When using the ``suds`` protocol an exception
+    is raised if the document could not be found in the store while in all other
+    cases ``None`` is returned instead.
+  * Documents in a ``DocumentStore`` are now accessed as bytes instead file-like
+    stream objects.
+  * Made more ``DocumentStore`` functions private.
+
+* Corrected error message displayed in case of a transport error.
+* Many unit tests updated and added.
+* Unit tests may now be run using the setuptools ``setup.py test`` command.
+
+  * Note that this method does not allow passing additional pytest testing
+    framework command-line arguments. To specify any such parameters invoke the
+    pytest framework directly, e.g. using ``python -m pytest`` in the project's
+    root folder.
+
+* Internal code cleanup.
+
+  * Removed undocumented, unused and untested ``binding.replyfilter``
+    functionality.
+  * Binding classes no longer have anything to do with method independent Fault
+    element processing.
+  * Removed SoapClient ``last_sent()`` and ``last_received()`` functions.
+  * Fixed file closing in ``reader.py`` & ``cache.py`` modules - used files now
+    closed explicitly in case of failed file operations instead of relying on
+    the Python GC to close them 'some time later on'.
+  * Fixed silently ignoring internal exceptions like ``KeyboardInterrupt`` in
+    the ``cache.py`` module.
+  * Removed unused ``Cache`` module ``getf()`` & ``putf()`` functions.
+    ``getf()`` left only in ``FileCache`` and its derived classes.
+
+version 0.4.1 jurko 4 (2012-04-17)
+----------------------------------
+
+* Based on revision 712 (1e48fd79a1fc323006826439e469ba7b3d2b5a68) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Installation procedure requires the ``distribute`` Python package to be
+    installed on the system.
+  * Tested in the following environments:
+
+    * Python 2.7.1/x64 on Windows XP/SP3/x64.
+    * Python 3.2.2/x64 on Windows XP/SP3/x64.
+
+* Cleaned up how the distribution package maintainer name string is specified so
+  it does not contain characters causing the setup procedure to fail when run
+  using Python 3+ on systems using CP1250 or UTF-8 as their default code-page.
+* Internal cleanup - renamed bounded to single_occurrence and unbounded to
+  multi_occurrence.
+* Original term unbounded meant that its object has more than one occurrence
+  while its name inferred that 'it has no upper limit on its number of
+  occurrences'.
+
+version 0.4.1 jurko 3 (2011-12-26)
+----------------------------------
+
+* Based on revision 711 (1be817c8a7672b001eb9e5cce8842ebd0bf424ee) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Installation procedure requires the ``distribute`` Python package to be
+    installed on the system.
+  * Tested in the following environments:
+
+    * Python 2.7.1/x86 on Windows XP/SP3/x86.
+    * Python 3.2.2/x86 on Windows XP/SP3/x86.
+
+* Operation parameter specification string no longer includes a trailing comma.
+* ``suds.xsd.xsbasic.Enumeration`` objects now list their value in their string
+  representation.
+* ``suds.sudsobject.Metadata`` ``__unicode__()``/``__str__()``/``__repr__()``
+  functions no longer raise an ``AttributeError`` when the object is not empty.
+* Fixed a bug with ``suds.xsd.sxbasic.TypedContent.resolve()`` returning an
+  incorrect type when called twice on the same node referencing a builtin type
+  with the parameter ``nobuiltin=True``.
+* Added more test cases.
+
+version 0.4.1 jurko 2 (2011-12-24)
+----------------------------------
+
+* Based on revision 711 (1be817c8a7672b001eb9e5cce8842ebd0bf424ee) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Installation procedure requires the ``distribute`` Python package to be
+    installed on the system.
+  * Tested in the following environments:
+
+    * Python 2.7.1/x86 on Windows XP/SP3/x86.
+    * Python 3.2.2/x86 on Windows XP/SP3/x86.
+
+* Fixed a bug causing converting a ``suds.client.Client`` object to a string to
+  fail & raise an ``IndexError`` exception.
+
+  * Changed the way ``suds.client.Client to-string`` conversion outputs build
+    info. This fixes a bug in the original ``0.4.1 jurko 1`` forked project
+    release causing printing out a ``suds.client.Client`` object to raise an
+    exception due to the code in question making some undocumented assumptions
+    on how the build information string should be formatted.
+
+version 0.4.1 jurko 1 (2011-12-24)
+----------------------------------
+
+* Based on revision 711 (1be817c8a7672b001eb9e5cce8842ebd0bf424ee) from the
+  original ``suds`` Python library development project's Subversion repository.
+
+  * Last officially packaged & released ``suds`` Python library version - 0.4.1.
+
+* Supported Python versions.
+
+  * Intended to work with Python 2.4+.
+  * Basic sources prepared for Python 2.x.
+  * For using Python 3 the sources first processed by the Python ``py2to3`` tool
+    during the setup procedure.
+  * Installation procedure requires the ``distribute`` Python package to be
+    installed on the system.
+  * Tested in the following environments:
+
+    * Python 2.7.1/x86 on Windows XP/SP3/x86.
+    * Python 3.2.2/x86 on Windows XP/SP3/x86.
+
+* Added Python 3 support:
+
+  * Based on patches integrated from a Mercurial patch queue maintained by
+    `Bernhard Leiner <https://bitbucket.org/bernh/suds-python-3-patches>`_.
+
+    * Last collected patch series commit:
+      ``96ffba978d5c74df28846b4273252cf1f94f7c78``.
+
+  * Original sources compatible with Python 2. Automated conversion to Python 3
+    sources during setup.
+
+    * Automated conversion implemented by depending on the ``distribute`` setup
+      package.
+
+* Made ``suds`` work with operations taking choice parameters.
+
+  * Based on a patch by michaelgruenewald & bennetb01 attached to ticket `#342
+    <http://fedorahosted.org/suds/ticket/342>`_ on the original ``suds`` project
+    issue tracker. Comments listed related to that ticket seem to indicate that
+    there may be additional problems with this patch but so far we have not
+    encountered any.
+
+* Fixed the ``DateTimeTest.testOverflow`` test to work correctly in all
+  timezones.
+
+  * This test would fail if run directly when run on a computer with a positive
+    timezone time adjustment while it would not fail when run together with all
+    the other tests in this module since some other test would leave behind a
+    nonpositive timezone adjustment setting. Now the test explicitly sets its
+    own timezone time adjustment to a negative value.
+  * Fixes a bug referenced in the original ``suds`` project issue tracker as
+    ticket `#422 <http://fedorahosted.org/suds/ticket/422>`_.
+
+* Corrected accessing ``suds.xsd.sxbase.SchemaObject`` subitems by index.
+
+  * Fixes a bug referenced in the original ``suds`` project issue tracker as
+    ticket `#420 <http://fedorahosted.org/suds/ticket/420>`_.
+
+* Internal code & project data cleanup.
+
+  * Extracted version information into a separate module.
+  * Added missing release notes for the original ``suds`` Python library
+    project.
+  * Ported unit tests to the ``pytest`` testing framework.
+  * Cleaned up project tests.
+
+    * Separated standalone tests from those requiring an external web service.
+    * Added additional unit tests.
+    * Added development related documentation - ``HACKING.txt``.
+    * Setup procedure cleaned up a bit.
+
+* Known defects.
+
+  * Converting a ``suds.client.Client`` object to a string fails & raises an
+    ``IndexError`` exception.
+
+
+Original suds library release notes
+=================================================
+
+**version 0.4.1 (2010-10-15)**
+
+* <undocumented>
+
+**version 0.4 (2010-09-08)**
+
+* Fix spelling errors in spec description.
+* Fix source0 URL warning.
+* Updated caching to not cache intermediate WSDLs.
+* Added ``DocumentCache`` which caches verified XML documents as text. User can
+  choose.
+* Added ``cachingpolicy`` option to allow user to specify whether to cache XML
+  documents or WSDL objects.
+* Provided for repeating values in reply for message parts consistent with the
+  way this is handled in nested objects.
+* Added ``charset=utf-8`` to stock content-type HTTP header.
+* Added ``<?xml version="1.0" encoding="UTF-8"?>`` to outgoing SOAP messages.
+* Detection of faults in successful (http=200) replies and raise ``WebFault``.
+  Search for ``<soapenv:Fault/>``.
+* Add plugins facility.
+* Fixed Tickets: #251, #313, #314, #334.
+
+**version 0.3.9 (2009-12-17)**
+
+* Bumped python requires to 2.4.
+* Replaced stream-based caching in the transport package with document-based
+  caching.
+* Caches pickled ``Document`` objects instead of XML text. 2x Faster!
+* No more SAX parsing exceptions on damaged or incomplete cached files.
+* Cached WSDL objects. Entire ``Definitions`` object including contained
+  ``Schema`` object cached via pickle.
+* Copy of SOAP encoding schema packaged with ``suds``.
+* Refactor ``Transports`` to use ``ProxyHandler`` instead of
+  ``urllib2.Request.set_proxy()``.
+* Added WSSE enhancements ``<Timestamp/>`` and ``<Expires/>`` support. See:
+  Timestamp token.
+* Fixed Tickets: #256, #291, #294, #295, #296.
+
+**version 0.3.8 (2009-12-09)**
+
+* Included Windows NTLM Transport.
+* Add missing ``self.messages`` in ``Client.clone()``.
+* Changed default behavior for WSDL ``PartElement`` to be optional.
+* Add support for services/ports defined without ``<address/>`` element in WSDL.
+* Fix ``sax.attribute.Element.attrib()`` to find by name only when ns is not
+  specified; renamed to ``Element.getAttribute()``.
+* Update ``HttpTransport`` to pass timeout parameter to urllib2 open() methods
+  when supported by urllib2.
+* Add ``null`` class to pass explicit NULL values for parameters and optional
+  elements.
+* SOAP encoded array ``soap-enc:Array`` enhancement for rpc/encoded. Arrays
+  passed as python arrays - works like document/literal now. No more using the
+  factory to create the Array. Automatically includes ``arrayType`` attribute.
+  E.g. ``soap-enc:arrayType="Array[2]"``.
+* Reintroduced ability to pass complex (objects) using python dict instead of
+  ``suds`` object via factory.
+* Fixed tickets: #84, #261, #262, #263, #265, #266, #278, #280, #282.
+
+**version 0.3.7 (2009-10-16)**
+
+* Better SOAP header support
+* Added new transport ``HttpAuthenticated`` for active (not passive) basic
+  authentication.
+* New options (``prefixes``, ``timeout``, ``retxml``).
+* WSDL processing enhancements.
+* Expanded builtin XSD type support.
+* Fixed ``<xs:include/>``.
+* Better XML ``date``/``datetime`` conversion.
+* ``Client.clone()`` method added for lightweight copy of client object.
+* XSD processing fixes/enhancements.
+* Better ``<simpleType/>`` by ``<xs:restriction/>`` support.
+* Performance enhancements.
+* Fixed tickets: #65, #232, #233, #235, #241, #242, #244, #247, #254, #254,
+  #256, #257, #258.
+
+**version 0.3.6 (2009-04-31)**
+
+* Change hard coded ``/tmp/suds`` to ``tempfile.gettempdir()`` and create
+  ``suds/`` on demand.
+* Fix return type for ``Any.get_attribute()``.
+* Update HTTP caching to ignore ``file://`` URLs.
+* Better logging of messages when only the reply is injected.
+* Fix ``XInteger`` and ``XFloat`` types to translate returned arrays properly.
+* Fix ``xs:import`` schema with same namespace.
+* Update parser to not load external references and add ``Import.bind()`` for
+  ``XMLSchema.xsd`` location.
+* Add schema doctor - used to patch XSDs at runtime (see ``Option.doctor``).
+* Fix deprecation warnings in python 2.6.
+* Add behavior for ``@default`` defined on ``<element/>``.
+* Change ``@xsi:type`` value to always be qualified for doc/literal (reverts
+  0.3.5 change).
+* Add ``Option.xstq`` option to control when ``@xsi:type`` is qualified.
+* Fixed Tickets: #64, #129, #205, #206, #217, #221, #222, #224, #225, #228,
+  #229, #230.
+
+**version 0.3.5 (2009-04-16)**
+
+* Adds HTTP caching. Default is (1) day. Does not apply to method invocation.
+  See: documentation for details.
+* Removed checking fedora version check in spec since no longer building < fc9.
+* Updated makefile to roll tarball with tar.sh.
+* Moved bare/wrapped determination to WSDL for document/literal.
+* Refactored ``Transport`` into a package (provides better logging of HTTP
+  headers).
+* Fixed Tickets: #207, #209, #210, #212, #214, #215.
+
+**version 0.3.4 (2009-02-24)**
+
+* Static (automatic)
+  ``Import.bind('http://schemas.xmlsoap.org/soap/encoding/')``, users no longer
+  need to do this.
+* Basic ws-security with {{{UsernameToken}}} and clear-text password only.
+* Add support for ``sparse`` SOAP headers via passing dictionary.
+* Add support for arbitrary user defined SOAP headers.
+* Fixes service operations with multiple SOAP header entries.
+* Schema loading and dereferencing algorithm enhancements.
+* Nested SOAP multirefs fixed.
+* Better (true) support for ``elementFormDefault="unqualified"`` provides more
+  accurate namespacing.
+* WSDL part types no longer default to WSDL ``targetNamespace``.
+* Fixed Tickets: #4, #6, #21, #32, #62, #66, #71, #72, #114, #155, #201.
+
+**version 0.3.3 (2008-11-31)**
+
+* No longer installs (tests) package.
+* Implements API-3 proposal (https://fedorahosted.org/suds/wiki/Api3Proposal).
+
+  * Pluggable transport.
+  * Keyword method arguments.
+  * Basic HTTP authentication in default transport.
+
+* Add namespace prefix normalization in SOAP message.
+* Better SOAP message pruning of empty nodes.
+* Fixed Tickets: #51 - #60.
+
+**version 0.3.2 (2008-11-07)**
+
+* SOAP {{{MultiRef}}} support ``(1st pass added r300)``.
+* Add support for new schema tags:
+
+  * ``<xs:include/>``
+  * ``<xs:simpleContent/>``
+  * ``<xs:group/>``
+  * ``<xs:attributeGroup/>``
+
+* Added support for new xs <--> python type conversions:
+
+  * ``xs:int``
+  * ``xs:long``
+  * ``xs:float``
+  * ``xs:double``
+
+* Revise marshaller and binding to further sharpen the namespacing of nodes
+  produced.
+* Infinite recursion fixed in ``xsd`` package ``dereference()`` during schema
+  loading.
+* Add support for ``<wsdl:import/>`` of schema files into the WSDL root
+  ``<definitions/>``.
+* Fix double encoding of (&).
+* Add Client API:
+
+  * ``setheaders()`` - same as keyword but works for all invocations.
+  * ``addprefix()`` - mapping of namespace prefixes.
+  * ``setlocation()`` - override the location in the WSDL; same as keyword
+    except for all calls.
+  * ``setproxy()`` - same as proxy keyword but for all invocations.
+
+* Add proper namespace prefix for SOAP headers.
+* Fixed Tickets: #5, #12, #34, #37, #40, #44, #45, #46, #48, #49, #50, #51.
+
+**version 0.3.1 (2008-10-01)**
+
+* Quick follow up to the 0.3 release that made working multi-port service
+  definitions harder then necessary. After consideration (and a good night
+  sleep), it seemed obvious that a few changes would make this much easier:
+
+  1) filter out the non-SOAP bindings - they were causing the real trouble;
+  2) since most servers are happy with any of the SOAP bindings (SOAP 1.1 and
+     1.2), ambiguous references to methods when invoking then without the port
+     qualification will work just fine in almost every case. So, why not just
+     allow ``suds`` to select the port. Let us not make the user do it when it
+     is not necessary. In most cases, users on 0.2.9 and earlier will not have
+     to update their code when moving to 0.3.1 as they might have in 0.3.
+
+**version 0.3 (2008-09-30)**
+
+* Extends the support for multi-port services introduced in 0.2.9. This
+  addition, provides for multiple services to define the *same* method and
+  ``suds`` will handle it properly. See section 'SERVICES WITH MULTIPLE PORTS:'.
+* Add support for multi-document document/literal SOAP binding style. See
+  section 'MULTI-DOCUMENT Document/Literal:'.
+* Add support for ``xs:group``, ``xs:attributeGroup`` tags.
+* Add ``Client.last_sent()`` and ``Client.last_received()``.
+
+**version 0.2.9 (2008-09-09)**
+
+* Support for multiple ports within a service.
+* Attribute references ``<xs:attribute ref=""/>``.
+* Make XML special character encoder in sax package - pluggable.
+
+**version 0.2.8 (2008-08-28)**
+
+* Update document/literal binding to always send the document root referenced by
+  the ``<part/>``. After yet another review of the space and user input, seems
+  like the referenced element is ALWAYS the document root.
+* Add support for 'binding' ``schemaLocation``s to namespace-uri. This is for
+  imports that do not specify a ``schemaLocation`` and still expect the schema
+  to be downloaded. E.g. Axis references
+  'http://schemas.xmlsoap.org/soap/encoding/' without a schemaLocation. So, by
+  doing this::
+
+    >
+    > from suds.xsd.sxbasic import Import
+    > Import.bind('http://schemas.xmlsoap.org/soap/encoding/')
+    >
+
+  The schema is bound to a ``schemaLocation`` and it is downloaded.
+* Basic unmarshaller does not need a `schema`. Should have been removed during
+  refactoring but was missed.
+* Update client to pass kwargs to ``send()`` and add ``location`` kwarg for
+  overriding the service location in the WSDL.
+* Update marshaller to NOT emit XML for object attributes that represent
+  elements and/or attributes that are *both* optional and ``value=None``.
+
+  * Update factory (builder) to include all attributes.
+  * Add ``optional()`` method to ``SchemaObject``.
+
+* Update WSDL to override namespace in operation if specified.
+* Fix schema loading issue - build all schemas before processing imports.
+* Update packaging in preparation of submission to fedora.
+
+**version 0.2.7 (2008-08-11)**
+
+* Add detection/support for document/literal - wrapped and unwrapped.
+* Update document/literal {wrapped} to set document root (under <body/>) to be
+  the wrapper element (w/ proper namespace).
+* Add support for ``<sequence/>``, ``<all/>`` and ``<choice/>`` having
+  ``maxOccurs`` and have the. This causes the unmarshaller to set values for
+  elements contained in an unbounded collection as a list.
+* Update client.factory (builder) to omit children of ``<choice/>`` since the
+  'user' really needs to decide which children to include.
+* Update flattening algorithm to prevent re-flattening of types from imported
+  schemas.
+* Adjustments to flattening/merging algorithms.
+
+**version 0.2.6 (2008-08-05)**
+
+* Fix ENUMs broken during ``xsd`` package overhaul.
+* Fix type as defined in ticket #24.
+* Fix duplicate param names in method signatures as reported in ticket #30.
+* Suds licensed as LGPL.
+* Remove logging setup in ``suds.__init__()`` as suggested by patch in ticket
+  #31. Users will now need to configure the logger.
+* Add support for ``Client.Factory.create()`` alt: syntax for fully qualifying
+  the type to be built as: ``{namespace}name``. E.g.::
+
+    > client.factory.create('{http://blabla.com/ns}Person')
+
+**version 0.2.5 (2008-08-01)**
+
+* Overhauled the ``xsd`` package. This new (merging) approach is simpler and
+  should be more reliable and maintainable. Also, should provide better
+  performance since the merged schema performs lookups via dictionary lookup.
+  This overhaul should fix current ``TypeNotFound`` and ``<xs:extension/>``
+  problems, I hope :-).
+* Fixed dateTime printing bug.
+* Added infinite recursion prevention in ``builder.Builder`` for XSD types that
+  contain themselves.
+
+**version 0.2.4 (2008-07-28)**
+
+* Added support for WSDL imports: ``<wsdl:import/>``.
+* Added support for XSD<->python type conversions (thanks: Nathan Van Gheem)
+  for:
+
+  * ``xs:date``
+  * ``xs:time``
+  * ``xs:dateTime``
+
+* Fixed:
+
+  * Bug: Schema ``<import/>`` with ``schemaLocation`` specified.
+  * Bug: Namespaces specified in service description not valid until client/
+    proxy is printed.
+
+**version 0.2.3 (2008-07-23)**
+
+* Optimizations.
+
+**version 0.2.2 (2008-07-08)**
+
+* Update exceptions to be more /standard/ python by using
+  ``Exception.__init__()`` to set ``Exception.message`` as suggested by ticket
+  #14; update bindings to raise ``WebFault`` passing (p).
+* Add capability in bindings to handle multiple root nodes in the returned
+  values; returned as a composite object unlike when lists are returned.
+* Fix ``soapAction`` to be enclosed by quotes.
+* Add support for ``<xs:all/>``.
+* Fix ``unbounded()`` method in ``SchemaObject``.
+* Refactored schema into new ``xsd`` package. Files just getting too big. Added
+  ``execute()`` to ``Query`` and retrofitted ``suds`` to ``execute()`` query
+  instead of using ``Schema.find()`` directly. Also, moved hokey ``start()``
+  methods from schema, as well as, query incrementation.
+* Add ``inject`` keyword used to ``inject`` outbound SOAP messages and/or
+  inbound reply messages.
+* Refactored SoapClient and
+
+  1) rename ``send()`` to ``invoke(``)
+  2) split message sending from ``invoke()`` and place in ``send()``
+
+* Add ``TestClient`` which allows for invocation kwargs to have ``inject={'msg=,
+  and reply='}`` for message and reply injection.
+* Add ``Namespace`` class to ``sax`` for better management of namespace
+  behavior; retrofix ``suds`` to import and use ``Namespace``.
+* Change the default namespace used to resolve referenced types (having
+  attributes ``@base=""``, ``@type=""``) so that when no prefix is specified:
+  uses XML (node) namespace instead of the ``targetNamespace``.
+* Apply fix as defined by davidglick at onenw.org in ticket #13.
+* Update service definition to print to display service methods as
+  ``my_method(xs:int arg0, Person arg1)`` instead of ``my_method(arg0{xs:int},
+  arg1{Person})`` which is more like traditional method signatures.
+* Add XSD/python type conversion to unmarshaller (``XBoolean`` only); refactor
+  unmarshaller to use ``Content`` class which makes APIs cleaner, adds symmetry
+  between marshaller(s) and unmarshaller(s), provides good mechanism for
+  schema-property based type conversions.
+* Refactored marshaller with Appenders; add ``nobuiltin`` flag to ``resolve()``
+  to support fix for ``returned_type()`` and ``returned_collection()`` in
+  bindings.
+* Add support for (202, 204) HTTP codes.
+* Add ``XBoolean`` and mappings; add ``findattr()`` to ``TreeResolver`` in
+  preparation for type conversions.
+* Updated schema and schema property loading (deep recursion stopped); Changed
+  ``Imported`` schemas so then no longer copy imported schemas, rather the
+  import proxies find requests; Add ``ServiceDefinition`` class which provides
+  better service inspection; also provides namespace mapping and show types;
+  schema property API simplified; support for ``xs:any`` and ``xs:anyType``
+  added; Some schema lookup problems fixed; Binding classes refactored slightly;
+  A lot of debug logging added (might have to comment some out for performance -
+  some of the args are expensive).
+* Add ``sudsobject.Property``; a property is a special ``Object`` that contains
+  a ``value`` attribute and is returned by the ``Builder`` (factory) for
+  schema-types without children such as: ``<element/>`` and ``<simpleType/>``;
+  ``Builder``, ``Marshaller`` and ``Resolver`` updated to handle ``Properties``;
+  ``Resolver`` and ``Schema`` also updated to handle attribute lookups (this was
+  missing).
+* Add groundwork for user defined SOAP headers.
+* Fix ``elementFormDefault`` per ticket #7
+* Remove unused kwargs from bindings; cache bindings in WSDL; retrofit legacy
+  ``ServiceProxy`` to delegate to {new} ``Client`` API; remove keyword
+  ``nil_supported`` in favor of natural handling by ``nillable`` attribute on
+  ``<element/>`` within schemas.
+* Add support for ``<element/>`` attribute flags (``nillable`` and ``form``).
+* Add the ``Proxy`` (2nd generation API) class.
+* Add accessor/conversion functions so that users do not need to access
+  ``__x__`` attributes. Also add ``todict()`` and ``get_items()`` for easy
+  conversion to dictionary and iteration.
+* Search top-level elements for ``@ref`` before looking deeper.
+* Add ``derived()`` to ``SchemaObject``. This is needed to ensure that all
+  derived types (WSDL classes) are qualified by ``xsi:type`` without specifying
+  the ``xsi:type`` for all custom types as did in earlier ``suds`` releases.
+  Update the literal marshaller to only add the ``xsi:type`` when the type needs
+  to be specified.
+* Change ns promotion in ``sax`` to prevent ns promoted to parent when parent
+  has the prefix.
+* Changed binding ``returned_type()`` to return the (unresolved) ``Element``.
+* In order to support the new features and fix reported bugs, I'm in the process
+  of refactoring and hopefully evolving the components in ``suds`` that provide
+  the input/output translations:
+
+  * ``Builder`` (translates: XSD objects => python objects)
+  * ``Marshaller`` (translates: python objects => XML/SOAP)
+  * ``Unmarshaller`` (translates: XML/SOAP => python objects)
+
+  This evolution will provide better symmetry between these components as
+  follows:
+
+  The ``Builder`` and ``Unmarshaller`` will produce python (subclass of
+  ``sudsobject.Object``) objects with:
+
+  * ``__metadata__.__type__`` = XSD type (``SchemaObject``)
+  * subclass name (``__class__.__name__``) = schema-type name
+
+  and
+
+  The ``Marshaller``, while consuming python objects produced by the ``Builder``
+  or ``Unmarshaller``, will leverage this standard information to produce the
+  appropriate output (XML/SOAP).
+
+  The 0.2.1 code behaves *mostly* like this but ... not quite. Also, the
+  implementations have some redundancy.
+
+  While doing this, it made sense to factor out the common schema-type "lookup"
+  functionality used by the ``Builder``, ``Marshaller`` and ``Unmarshaller``
+  classes into a hierarchy of ``Resolver`` classes. This reduces the complexity
+  and redundancy of the ``Builder``, ``Marshaller`` and ``Unmarshaller`` classes
+  and allows for better modularity. Once this refactoring was complete, the
+  difference between the literal/encoded ``Marshallers`` became very small.
+  Given that the amount of code in the ``bindings.literal`` and
+  ``bindings.encoded`` packages was small (and getting smaller) and in the
+  interest of keeping the ``suds`` code base compact, I moved all of the
+  marshalling classes to the ``bindings.marshaller`` module. All of the
+  ``bindings.XX`` sub-packages will be removed.
+
+  The net effect:
+
+  All of the ``suds`` major components:
+
+  * client (old: service proxy)
+  * WSDL
+
+    * schema (xsd package)
+    * resolvers
+
+  * output (marshalling)
+  * builder
+  * input (unmarshalling)
+
+  Now have better:
+
+  * modularity
+  * symmetry with regard to ``Object`` metadata.
+  * code re-use (< 1% code duplication --- I hope)
+  * looser coupling
+
+  and better provide for the following features/bug-fix:
+
+  * Proper level of XML element qualification based on ``<schema
+    elementFormDefault=""/>`` attribute. This will ensure that when
+    ``elementFormDefault="qualified"``, ``suds`` will include the proper
+    namespace on root elements for both literal and encoded bindings. In order
+    for this to work properly, the literal marshaller (like the encoded
+    marshaller) needed to be schema-type aware. Had I added the same schema-type
+    lookup as the encoded marshaller instead of the refactoring described above,
+    the two classes would have been almost a complete duplicate of each other
+    :-(
+
+* The builder and unmarshaller used the ``schema.Schema.find()`` to resolve
+  schema-types. They constructed a path as ``person.name.first`` to resolve
+  types in proper context. Since ``Schema.find()`` was stateless, it resolved
+  the intermediate path elements on every call. The new resolver classes are
+  stateful and resolve child types *much* more efficiently.
+* Prevent name collisions in ``sudsobject.Object`` like the ``items()`` method.
+  I've moved all methods (including class methods) to a ``Factory`` class that
+  is included in the ``Object`` class as a class attr (``__factory__``). Now
+  that *all* attributes have python built-in naming, we should not have any more
+  name collisions. This of course assumes that no WSDL/schema entity names will
+  have a name with the python built-in naming convention but I have to draw the
+  line somewhere. :-)
+
+**version 0.2.1 (2008-05-08)**
+
+* Update the ``schema.py`` ``SchemaProperty`` loading sequence so that the
+  schema is loaded in 3 steps:
+
+  1) Build the raw tree.
+  2) Resolve dependencies such as ``@ref`` and ``@base``.
+  3) Promote grandchildren as needed to flatten (denormalize) the tree.
+
+  The WSDL was also changed to only load the schema once and store it. The
+  schema collection was changed to load schemas in 2 steps:
+
+  1) Create all raw schema objects.
+  2) Load schemas.
+
+  This ensures that local imported schemas can be found when referenced out of
+  order. The ``sax.py`` ``Element`` interface changed: ``attribute()`` replaced
+  by ``get()`` and ``set()``. Also, ``__getitem__()`` and ``__setitem__()`` can
+  be used to access attribute values. Epydocs updated for ``sax.py``. And ...
+  last ``<element ref=/>`` now supported properly.
+
+* Fix logging by: NOT setting to info in ``suds.__init__.logger()``; set handler
+  on root logger only; moved logger (log) from classes to modules and use
+  __name__ for logger name. NOTE: This means that to enable SOAP message logging
+  one should use::
+
+    >
+    > logger('suds.serviceproxy').setLevel(logging.DEBUG)
+    >
+
+  instead of::
+
+    >
+    > logger('serviceproxy').setLevel(logging.DEBUG)
+    >
+
+* Add support for XSD schema ``<attribute/>`` nodes which primarily affects
+  objects returned by the ``Builder``.
+* Update ``serviceproxy.py:set_proxies()`` to log ``DEBUG`` instead of ``INFO``.
+* Enhance schema ``__str__()`` to show both the raw XML and the model (mostly
+  for debugging).
+
+**version 0.2 (2008-04-28)**
+
+* Contains the first cut at the rpc/encoded SOAP style.
+* Replaced ``Property`` class with ``suds.sudsobject.Object``. The ``Property``
+  class was developed a long time ago with a slightly different purpose. The
+  ``suds`` ``Object`` is a simpler (more straight forward) approach that
+  requires less code and works better in the debugger.
+* The ``Binding`` (and the encoding) is selected on a per-method basis which is
+  more consistent with the WSDL. In <= 0.1.7, the binding was selected when
+  the ``ServiceProxy`` was constructed and used for all service methods. The
+  binding was stored as ``self.binding``. Since the WSDL provides for a separate
+  binding style and encoding for each operation, ``suds`` needed to be change to
+  work the same way.
+* The ``nil_supported`` and ``faults`` flag(s) passed into the service proxy
+  using \**kwargs. In addition to these flags, a ``http_proxy`` flag has been
+  added and is passed to the ``urllib2.Request`` object. The following args are
+  supported:
+
+  * ``faults`` = Raise faults raised by server (default:``True``), else return
+    tuple from service method invocation as (HTTP code, object).
+  * ``nil_supported`` = The bindings will set the ``xsi:nil="true"`` on nodes
+    that have a ``value=None`` when this flag is ``True`` (default:``True``).
+    Otherwise, an empty node ``<x/>`` is sent.
+  * ``proxy`` = An HTTP proxy to be specified on requests (default:``{}``). The
+    proxy is defined as ``{protocol:proxy,}``.
+
+* HTTP proxy supported (see above).
+* ``ServiceProxy`` refactored to delegate to a ``SoapClient``. Since the service
+  proxy exposes web services via ``getattr()``, any attribute (including
+  methods) provided by the ``ServiceProxy`` class hides WS operations defined by
+  the WSDL. So, by moving everything to the ``SoapClient``, WSDL operations are
+  no longer hidden without having to use *hokey* names for attributes and
+  methods in the service proxy. Instead, the service proxy has ``__client__``
+  and ``__factory__`` attributes (which really should be at low risk for name
+  collision). For now the ``get_instance()`` and ``get_enum()`` methods have not
+  been moved to preserve backward compatibility. Although, the preferred API
+  change would to replace::
+
+    > service = ServiceProxy('myurl')
+    > person = service.get_instance('person')
+
+  with something like::
+
+    > service = ServiceProxy('myurl')
+    > person = service.__factory__.get_instance('person')
+
+  After a few releases giving time for users to switch the new API, the
+  ``get_instance()`` and ``get_enum()`` methods may be removed with a notice in
+  big letters.
+* Fixed problem where a WSDL does not define a ``<schema/>`` section and
+  ``suds`` can not resolve the prefixes for the
+  ``http://www.w3.org/2001/XMLSchema`` namespace to detect builtin types such as
+  ``xs:string``.
+
+**version 0.1.7 (2008-04-08)**
+
+* Added ``Binding.nil_supported`` to control how property values (out) =
+  ``None`` and empty tag (in) are processed.
+
+  * ``service.binding.nil_supported = True`` -- means that property values =
+    ``None`` are marshalled (out) as ``<x xsi:nil=true/>`` and <x/> is
+    unmarshalled as ``''`` and ``<x xsi:nil/>`` is unmarshalled as ``None``.
+  * ``service.binding.nil_supported = False`` -- means that property values =
+    ``None`` are marshalled (out) as ``<x/>`` *and* ``<x xsi:nil=true/>`` is
+    unmarshalled as ``None``. The ``xsi:nil`` is really ignored.
+  * THE DEFAULT IS ``True``.
+
+* Sax handler updated to handle ``multiple character()`` callbacks when the sax
+  parser "chunks" the text. When the ``node.text`` is ``None``, the
+  ``node.text`` is set to the characters. Else, the characters are appended.
+  Thanks - 'andrea.spinelli at imteam.it'.
+* Replaced special ``text`` attribute with ``__text__`` to allow for natural
+  elements named "text".
+* Add unicode support by:
+
+  * Add ``__unicode__()`` to all classes with ``__str__()``.
+  * Replace all ``str()`` calls with ``unicode()``.
+  * ``__str__()`` returns UTF-8 encoded result of ``__unicode__()``.
+
+* XML output encoded as UTF-8 which matches the HTTP header and supports
+  unicode.
+* ``SchemaCollection`` changed to provide the ``builtin()`` and ``custom()``
+  methods. To support this, ``findPrefixes()`` was added to the ``Element`` in
+  ``sax.py``. This is a better approach anyway since the WSDL and schemas may
+  have many prefixes to 'http://www.w3.org/2001/XMLSchema'. Tested using both
+  doc/lit and rpc/lit bindings.
+* Refactored bindings packages from document & rpc to literal & encoded.
+* Contains the completion of *full* namespace support as follows:
+
+  * Namespace prefixes are no longer stripped from attribute values that
+    reference types defined in the WSDL.
+  * Schema's imported using ``<import/>`` should properly handle namespace and
+    prefix mapping and re-mapping as needed.
+  * All types are resolved, using fully qualified (w/ namespaces) lookups.
+  * ``Schema.get_type()`` supports paths with and without ns prefixes. When no
+    prefix is specified the type is matched using the schema's target
+    namespace.
+
+* Property maintains attribute names (keys) in the order added. This also means
+  that ``get_item()`` and ``get_names()`` return ordered values. Although, I
+  suspect ordering really needs to be done in the marshaller using the order
+  specified in the WSDL/schema.
+* Major refactoring of the ``schema.py``. The primary goals is preparation for
+  type lookups that are fully qualified by namespace. Once completed, the
+  prefixes on attribute values will no longer be stripped (purged). Change
+  summary:
+
+  1) ``SchemaProperty`` overlay classes created at ``__init__()`` instead of
+     on-demand.
+  2) schema imports performed by new ``Import`` class instead of by ``Schema``.
+  3) Schema loads top level properties using a factory.
+  4) All ``SchemaProperty`` /children/ lists are sorted by ``__cmp__()`` in
+     ``SchemaProperty`` derived classes. This ensures that types with the same
+     name are resolved in the following order (``Import``, ``Complex``,
+     ``Simple``, ``Element``).
+  5) All /children/ ``SchemaProperty`` lists are constructed at ``__init__()``
+     instead of on-demand.
+  6) The SchemaGroup created and WSDL class updated. This works better then
+     having the WSDL aggregate the ``<schema/>`` nodes which severs linkage to
+     the WSDL parent element that have namespace prefix mapping.
+  7) ``<import/>`` element handles properly in that both namespace remapping and
+     prefix re-mapping of the imported schema's ``targetNamespace`` and
+     associated prefix mapping - is performed. E.g. SCHEMA-A has prefix ``tns``
+     mapped as ``xmlns:tns=http://nsA`` and has
+     ``targetNamespace='http://nsA'``. SCHEMA-B is importing schema A and has
+     prefix ``abc`` mapped as ``xmlns:abc='http://nsABC'``. SCHEMA-B imports A
+     as ``<import namespace=http://nsB xxx
+     schemaLocation=http://nsA/schema-a.xsd>``. So, since SCHEMA-B will be
+     referencing elements of SCHEMA-A with prefix ``abc`` such as
+     ``abc:something``, SCHEMA-A's ``targetNamespace`` must be updated as
+     ``http://nsABC`` and all elements with ``type=tns:something`` must be
+     updated to be ``type=abc:something`` so they can be resolved.
+
+* Fixes unmarshalling problem where nodes are added to property as (text,
+  value). This was introduced when the bindings were refactored.
+* Fixed various ``Property`` print problems.
+
+Notes:
+
+  Thanks to Jesper Noehr of Coniuro for the majority of the rpc/literal binding
+  and for lots of collaboration on ``#suds``.
+
+**version 0.1.6 (2008-03-06)**
+
+* Provides proper handling of WSDLs that contain schema sections containing XSD
+  schema imports: ``<import namespace="" schemaLocation=""?>``. The referenced
+  schemas are imported when a ``schemaLocation`` is specified.
+* Raises exceptions for HTTP status codes not already handled.
+
+**version 0.1.5 (2008-02-21)**
+
+* Provides better logging in the modules get logger by hierarchal names.
+* Refactored as needed to truly support other bindings.
+* Add ``sax`` module which replaces ``ElementTree``. This is faster, simpler and
+  handles namespaces (prefixes) properly.
+
+**version 0.1.4 (2007-12-21)**
+
+* Provides for service method parameters to be ``None``.
+* Add proper handling of method params that are lists of property objects.
+
+**version 0.1.3 (2007-12-19)**
+
+* Fixes problem where nodes marked as a collection (``maxOccurs`` > 1) not
+  creating property objects with ``value=[]`` when mapped-in with < 2 values by
+  the ``DocumentReader``. Caused by missing the
+  ``bindings.Document.ReplyHint.stripns()` (which uses
+  ``DocumentReader.stripns()``) conversion to ``DocumentReader.stripn()`` now
+  returning a tuple ``(ns, tag)`` as of 0.1.2.
+
+**version 0.1.2 (2007-12-18)**
+
+* This release contains an update to property adds:
+
+  - ``Metadata`` support.
+  - Overrides: ``__getitem__``, ``__setitem__``, ``__contains__``.
+  - Changes property(reader|writer) to use the ``property.metadata`` to handle
+    namespaces for XML documents.
+  - Fixes ``setup.py`` requires.
+
+**version 0.1.1 (2007-12-17)**
+
+* This release marks the first release in fedora hosted.
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..5a0ee1e
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,1117 @@
+PRIORETIZED:
+=================================================
+
+(21.12.2011.)
+
+(+) * (Jurko) Prepare for the initial forked project release.
+(+)     * Add todo list.
+(+)     * Document how to access this forked project's development sources &
+(+)       released files.
+(+)         * Jurko's Mercurial repository hosted at BitBucket and accessible
+(+)           from 'https://bitbucket.org/jurko/suds'.
+(+)             * Already documented in README.
+(+)             * Add more details to HACKING.
+(+)         * Suds library Python 3 patches hosted in a Mercurial patch queue
+(+)           repository at BitBucket and accessible from
+(+)           'https://bitbucket.org/bernh/suds-python-3-patches'.
+(+)             * Already documented in HACKING.
+(+) * (Jurko) Minor stylistic changes & typo corrections.
+(+)     * Code.
+(+)         * 'tranparent' --> 'transparent'.
+(+)         * 'if  tns' --> 'if tns'.
+(+)         * 'docuemnt' --> 'document'.
+(+)         * '('restriction', 'any', 'list',)' --> '('restriction', 'any',
+(+)           'list')'.
+(+)             * And other unnecessary trailing tuple commas.
+(+)         * 'Qualfied' --> 'Qualified'.
+(+)         * 'Resolveds' --> 'Resolves'.
+(+)         * 'describe a port and it's list of methods' --> 'describe a port
+(+)           and its list of methods'.
+(+)         * 'dependancies' --> 'dependencies'.
+(+)         * 'imcoming' --> 'incoming'.
+(+)         * 'relavent' --> 'relevant'.
+(+)         * 'indicat' --> 'inidcat'.
+
+(22.12.2011.)
+
+(+) * (Jurko) Prepare for the initial forked project release.
+(+)     * Rename top level project documentation files to use the .txt extension
+(+)       to make them friendlier to Windows users.
+(+)     * Research release procedure.
+(+)         * Open PyPI account.
+(+)         * How to prepare a source distribution package.
+(+)             * Change author information.
+(+)             * Include tests.
+(+)             * Include all the top-level documentation files.
+(+)                 * 'README'.
+(+)                 * 'LICENSE'.
+(+)                 * 'HACKING'.
+(+)                 * 'TODO'.
+(+)             * Note the original project author in the package description.
+(+)             * Include correct license information.
+(+)             * See what the difference between author and maintainer
+(+)               information is and where it can be seen.
+(+)         * Try using 'setuptools_hg' to simplify specifying the project
+(+)           sources.
+(+)             * Failed when used under Python 3.
+(+)         * How to upload the prepared distribution packages.
+(+)             * Should upload a source distribution only.
+
+(23.12.2011.)
+
+(+) * (Jurko) Prepare for the initial forked project release.
+(+)     * Research release procedure.
+(+)         * How to upload the prepared distribution packages.
+(+)             * PyPI.
+
+(24.12.2011.)
+
+(+) * (Jurko) Prepare for the initial forked project release.
+(+)     * Research release procedure.
+(+)         * How to upload the prepared distribution packages.
+(+)             * BitBucket.
+(+)         * Document the project's official download URL.
+(+)     * Document how to access this forked project's development sources &
+(+)       released files.
+(+)         * Released project packages accessible from PyPI & BitBucket.
+(+)         * Installing the project using distribute or pip.
+(+)     * Document release procedure.
+(+)         * Version identification.
+(+)             * Remove the '(development)' suffix for official release builds.
+(+)             * Format '<base-suds-version> jurko #', e.g. '0.4.1 jurko 1'.
+(+)         * Tag in Hg.
+(+)             * Name the tag like 'release-<version-info>', e.g.
+(+)               'release-0.4.1 jurko 1'.
+(+)         * Prepare official releases based only on tagged commits.
+(+)             * Prepare source distribution package, register the new release
+(+)               at PyPI and upload the prepared source package.
+(+)                 * Run 'setup.py sdist register upload'.
+(+)             * Upload the prepared source package to the project site.
+(+)             * Archive the prepared source release locally if needed.
+(+)         * Next development version identification.
+(+)             * Bump up the forked project version counter.
+(+)             * Add back the '(development)' suffix.
+(+)     * Commit all local changes.
+(+) * (Jurko) Constructing a SOAP request containing data stored in a sequence
+(+)   inside a choice.
+(+)     * Test scenario (syntax not precise).
+(+)         <choice>
+(+)             <element "a" - string />
+(+)             <element "s">
+(+)                 <sequence>
+(+)                     <element "s1" - string />
+(+)                     <element "s2" - string />
+(+)                 </sequence>
+(+)             </element>
+(+)         </choice>
+(+)     * When 's' is None and 'a' is not - 'a' should be used.
+(+)     * When 'a' is None and 's' is not - 's' should be used.
+(+)     * When 's' is used, all of its subelements should be used independent of
+(+)       whether they are None or not.
+(+)     * Add related test.
+(+) * (Jurko) Prepare the '0.4.1 jurko 1' release.
+(+)     * Follow the documented release procedure.
+(+)         * Update version information.
+(+)         * Tag in Hg.
+(+)         * Upload the source package.
+(+)             * Project site.
+(+)             * PyPI.
+(+) * (Jurko) Fix getting a suds.client object's string representation when the
+(+)   client is initialized with the following WSDL. Calling 'str(client)'
+(+)   reports 'IndexError: list index out of range'.
+(+)     * WSDL.
+(+)         <?xml version='1.0' encoding='UTF-8'?>
+(+)         <wsdl:definitions targetNamespace="my-namespace"
+(+)         xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+(+)         xmlns:ns="my-namespace"
+(+)         xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+(+)           <wsdl:types>
+(+)             <xsd:schema targetNamespace="my-namespace"
+(+)             elementFormDefault="qualified"
+(+)             attributeFormDefault="unqualified"
+(+)             xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+(+)               <xsd:element name="Elemento">
+(+)                 <xsd:complexType>
+(+)                   <xsd:sequence>
+(+)                     <xsd:element name="x1" type="xsd:string" />
+(+)                     <xsd:element name="x2">
+(+)                       <xsd:complexType>
+(+)                         <xsd:choice>
+(+)                           <xsd:element name="u1" type="xsd:string" />
+(+)                           <xsd:element name="u2" type="xsd:string" />
+(+)                           <xsd:element name="u3" type="xsd:string" />
+(+)                         </xsd:choice>
+(+)                       </xsd:complexType>
+(+)                     </xsd:element>
+(+)                     <xsd:element name="x3" type="xsd:string" />
+(+)                   </xsd:sequence>
+(+)                 </xsd:complexType>
+(+)               </xsd:element>
+(+)             </xsd:schema>
+(+)           </wsdl:types>
+(+)           <wsdl:message name="fRequestMessage">
+(+)             <wsdl:part name="parameters" element="ns:Elemento" />
+(+)           </wsdl:message>
+(+)           <wsdl:portType name="dummyPortType">
+(+)             <wsdl:operation name="f">
+(+)               <wsdl:input message="ns:fRequestMessage" />
+(+)             </wsdl:operation>
+(+)           </wsdl:portType>
+(+)           <wsdl:binding name="dummy" type="ns:dummyPortType">
+(+)             <soap:binding style="document"
+(+)             transport="http://schemas.xmlsoap.org/soap/http" />
+(+)             <wsdl:operation name="f">
+(+)               <soap:operation soapAction="f" style="document" />
+(+)               <wsdl:input><soap:body use="literal" /></wsdl:input>
+(+)               <wsdl:output><soap:body use="literal" /></wsdl:output>
+(+)             </wsdl:operation>
+(+)           </wsdl:binding>
+(+)           <wsdl:service name="dummy">
+(+)             <wsdl:port name="dummy" binding="ns:dummy">
+(+)               <soap:address location="https://localhost/dummy" />
+(+)             </wsdl:port>
+(+)           </wsdl:service>
+(+)         </wsdl:definitions>
+(+)     * Research.
+(+)         * Caused by undocumented suds.client.Client behaviour - it was
+(+)           expecting a specifically formatted suds.__build__ string which was
+(+)           not the case with the forked project release.
+(+)     * Add a test.
+(+)     * Fix.
+(+)     * Plan preparing a patched release.
+(+) * (Jurko) Prepare the '0.4.1 jurko 2' release.
+(+)     * Update release notes.
+(+)     * Follow the documented release procedure.
+(+)         * Update version information.
+(+)         * Tag in Hg.
+(+)         * Upload the source package.
+(+)             * Project site.
+(+)             * PyPI.
+
+(25.12.2011.)
+
+(+) * (Jurko) Printing out a list of function parameters should not print an
+(+)   additional trailing comma after the last parameter.
+(+)     * Research.
+(+)         * suds.servicedefinition.ServiceDescription.description() code needs
+(+)           to be changed.
+(+)     * Prepare test.
+(+)     * Update code.
+(+) * (Jurko) 'suds.xsd.xsbasic.Enumeration' objects should list their value in
+(+)   their string representation.
+(+)     * Research.
+(+)     * Prepare test.
+(+)     * Update code.
+(+) * (Jurko) 'suds.sudsobject.Metadata' __unicode__()/__str__()/__repr__()
+(+)   functions should not raise an AttributeError.
+(+)     * Research.
+(+)         * There should be no need to access a 'suds.sudsobject.Metadata'
+(+)           object's __metadata__ member as done for 'suds.sudsobjects.Facade'
+(+)           class instances.
+(+)     * Prepare test.
+(+)     * Update code.
+
+(26.12.2011.)
+
+(+) * (Jurko) Clean up suds.xsd.sxbasic.TypedContent.resolve().
+(+)     * Research.
+(+)         * Base class resolve() behaviour.
+(+)         * Other resolve() functions in suds.
+(+)         * 'resolve()' related caching.
+(+)     * Clean up the SchemaObject resolve() implementations.
+(+)         * Caching not needed in TypedContent base classes.
+(+)     * Document.
+(+)         * Returns the same XSD node when the node does not have an
+(+)           explicitly specified external type.
+(+)         * When called for an XSD node with an explicitly specified external
+(+)           type returns that type's XSD node.
+(+) * (Jurko) Clean up suds.xsd.sxbasic.TypedContent.resolve().
+(+)     * Research WSDL structure related to the resolving type references.
+(+)         * 'type'.
+(+)         * 'ref'.
+(+)     * Prepare additional resolve() tests.
+(+)         * 'ref'.
+(+)             * Valid.
+(+)             * Recursive.
+(+)             * Invalid.
+(+)         * References to nodes referencing other nodes.
+(+)             * There seems to be no way to do this in WSDL so seems no
+(+)               reason to keep the complicated and potentially buggy
+(+)               recursive resolve() implementation.
+(+)     * Refactor the resolve() implementation to remove recursion.
+(+)         * Todo items obsoleted by this refactoring.
+(+)             * Prevent possible endless resolve() loops due to resolve()
+(+)               directly or indirectly returning the same TypedContent
+(+)               instance.
+(+)     * Refactor to cache the final resolved type instead of a possibly only
+(+)       partially resolved one when resolving without allowing resolving to
+(+)       builtin types.
+(+)         * Research.
+(+)         * Prepare test.
+(+)         * Update code.
+(+) * (Jurko) Check and remove detected potential unused imports if they are no
+(+)   longer needed.
+(+)     * splitPrefix.
+(+)     * DefinitionsReader.
+(+) * (Jurko) Prepare the '0.4.1 jurko 3' release.
+(+)     * Update release notes.
+(+)     * Follow the documented release procedure.
+(+)         * Update version information.
+(+)         * Tag in Hg.
+(+)         * Upload the source package.
+(+)             * Project site.
+(+)             * PyPI.
+(+) * (Jurko) Look into suds.xsd.sxbase.SchemaObject.unbounded(). It seems to
+(+)   return True even when the object is bounded with a max value greater than
+(+)   1.
+(+)     * Research.
+(+)     * Add tests.
+(+)         * 'min'.
+(+)         * 'max'.
+(+)         * 'optional'.
+(+)         * 'required'.
+(+)         * 'unbounded'.
+(+)     * Update code - rename unbounded to multi_occurrence.
+
+(27.12.2011.)
+
+(+) * (Jurko) Get calling a web service operation taking no parameters to work
+(+)   correctly.
+(+)     * Research.
+(+)         * Seems to work fine. The original problem triggering this task
+(+)           seems to have been caused by an invalid WSDL.
+(+)     * Add a task to add more detailed test cases for this.
+
+(17.04.2012.)
+
+(+) * (Jurko) Merge upstream changes from the original suds development
+(+)   repository.
+(+) * (Jurko) Update embedded author values so they do not include non-ASCII
+(+)   characters causing problems with the 'distribute' based setup procedure
+(+)   which erroneously assumes they have been prepared using the user's local
+(+)   code-page.
+(+) * (Jurko) Process received pull requests on BitBucket.
+(+) * (Jurko) Prepare the '0.4.1 jurko 4' release.
+(+)     * Update release notes.
+(+)     * Follow the documented release procedure.
+(+)         * Update version information.
+(+)         * Tag in Hg.
+(+)         * Upload the source package.
+(+)             * Project site.
+(+)             * PyPI.
+
+(28.02.2013.)
+
+(+) * (Jurko) Merge changes prepared by Juraj Ivanèiæ.
+(+)     * Update the original Hg repository containing Python 3 related fixes.
+(+) * (Jurko) Process received pull requests.
+
+(01.03.2013.)
+
+(+) * (Jurko) Sync with external related repositories.
+(+)     * 'https://bitbucket.org/palday/suds'.
+
+(27.03.2013.)
+
+(+) * (Jurko) Fix buggy Python 3 support patch related to reading the cache
+(+)   version.
+(+)     * The cache version file should be read as a text and not as a binary
+(+)       file.
+(+) * (Jurko) Fix test_enumeration_type_string_should_contain_its_value test
+(+)   under Python 2.
+(+) * (Jurko) Test & fix Python 2.4 compatibility.
+(+) * (Jurko) Fix input/output binding usage.
+(+)     * Incorrect binding was being used in several places.
+(+)     * Some of the uses related to processing the SOAP Fault error reporting
+(+)       element seem to be 'fake', needed only because of a a bit messy
+(+)       design. Planned to be fixed soon.
+
+(28.03.2013.)
+
+(+) * (Jurko) Add web service reply processing related unit tests.
+(+) * (Jurko) Remove undocumented, untested & unused binding.replyfilter
+(+)   functionality.
+(+) * (Jurko) Remove seeming unused SoapClient last_sent() and last_received()
+(+)   functionality.
+(+) * (Jurko) Add a test for unicode Fault data processing.
+(+) * (Jurko) Merge SoapClient failed() & succeeded() functions into the same
+(+)   process_reply() function.
+(+) * (Jurko) Make binding classes no longer have anything to do with method
+(+)   independent Fault element processing.
+(+) * (Jurko) Make reply XML processing check the namespace used for Envelope &
+(+)   Body elements.
+(+) * (Jurko) Make SOAP Fault processing check the namespaces used for all
+(+)   relevant tags.
+(+) * (Jurko) Make HTTP status code 200 XML replies containing a Fault element
+(+)   consistently reported as SOAP faults (plus issue a warning about the
+(+)   non-standard HTTP status code) both when reporting such faults using
+(+)   exceptions or by returning a (status, reason) tuple.
+(+)     * Currently this is done only when reporting them using exceptions.
+(+) * (Jurko) Make plugins received() & parsed() calls now process both success
+(+)   & error replies.
+(+) * (Jurko) SOAP fault reports with invalid Fault structure should not cause
+(+)   suds code to break with an 'invalid attribute' exception.
+(+) * (Jurko) SOAP fault reports with no <detail> tag (optional) should not
+(+)   cause suds code to break with an 'invalid attribute' exception when run
+(+)   with the suds 'faults' option set to false.
+(+) * (Jurko) Clean up message reply processing return codes with suds 'faults'
+(+)   option set to both true & false.
+(+) * (Jurko) Reorganize SimClient injection keywords.
+(+)     * 'msg' - request message.
+(+)     * 'reply' - reply message ('msg' must not be set).
+(+)     * 'status' - HTTP status code acompanying the 'reply' message.
+(+)     * 'description' - description string acompanying the 'reply' message.
+(+) * (Jurko) Check failing tests.
+(+)     * All tests now pass except for ones related to SOAP Fault unicode
+(+)       faultstring processing.
+
+(29.03.2013.)
+
+(+) * (Jurko) Sync with external related repositories.
+(+)     * 'https://bitbucket.org/blarghmatey/suds-blarghmatey'.
+(+) * (Jurko) Additional SOAP web service reply tests.
+(+) * (Jurko) Fix detected unicode problems.
+(+)     * Remove invalid WebFault fix merged from an external source.
+(+)     * All suds exception classes now contain unicode description messages.
+(+)     * Undo a hasty unicode related WebFault fix merged from an external
+(+)       source in revision 16b084e8eea6511981d171e63cada98b58720c38.
+(+)     * Rename smart_str class to byte_str and make it accept only string
+(+)       parameters.
+(+)     * Clean Python2/3 compatibility DocumentStore fix.
+(+)         * Now contains raw data instead of unicode strings.
+(+)         * This also fixes a problem where unicode data read from the
+(+)           DocumentStore would fail to be encoded using the default encoding,
+(+)           which would then get reported as 'document not found'.
+(+)     * SAX parser now accepts only byte string content instead of also
+(+)       accepting unicode strings containing latin1 characters only.
+(+)     * Make tests now specify their fixed wsdl & reply content as byte
+(+)       strings only.
+(+)     * Make all tests pass.
+(+)         * Python 2.4.
+(+)         * Python 2.7.3.
+(+)         * Python 3.2.3.
+(+) * (Jurko) Remove Python 2/3 unicode encoding compatibility support assuming
+(+)   that its encoded unicode representations contain only latin1 characters.
+(+)     * SoapClient 'location' cleanup.
+(+)         * Should be stored as a unicode object instead of being converted
+(+)           from its byte representation assuming it was encoded using a
+(+)           specific encoding, e.g. if read from a WSDL schema, it should be
+(+)           decoded the same as the rest of the WSDL schema.
+(+)     * Remove str2bytes() & bytes2str().
+(+) * (Jurko) Remove the str_to_utf8_in_py2() Python 2/3 unicode encoding
+(+)   compatibility support function as it no longer seems to be needed.
+(+) * (Jurko) Add tests for web service operation input & output element types.
+
+(30.03.2013.)
+
+(+) * (Jurko) Improve suds tests.
+
+(31.03.2013.)
+
+(+) * (Jurko) Add tests for wrapped suds operation input & output data.
+
+(01.04.2013.)
+
+(+) * (Jurko) Add tests for wrapped suds operation output data.
+(+) * (Jurko) Merge patches sent in by Juraj Ivanèiæ from PKE sistemi.
+(+)     * Add tests for disabled wrapped suds operation input & output data
+(+)       support.
+(+)     * Add code for disabling suds library wrapped parameter support.
+
+(02.04.2013.)
+
+(+) * (Jurko) Restriction support cleanup based on patches sent in by Juraj
+(+)   Ivanèiæ from PKE sistemi.
+(+)     * Research.
+(+)         * Not enough time to research this thoroughly and come up with a
+(+)           complete and well tested solution.
+(+)     * Prepare and commit related tests.
+(+)         * Mark the tests as 'expected to fail' & comment the reasons.
+(+)     * Commit as a separate unfinished private branch.
+(+) * (Jurko) Prepare the '0.4.1 jurko 5' release.
+(+)     * Update release notes.
+
+(08.05.2013.)
+
+(+) * (Jurko) Make suds construct SOAP requests with correct element namespaces
+(+)   when their XSD schema definition nodes reference other nodes with a
+(+)   different namespace.
+(+)     * Research.
+(+)     * Add test.
+(+)     * Implement.
+(+)     * Report back to Jens Arm from KabelDeutschland who reported the issue.
+(+) * (Jurko) Support specifying a custom DocumentStore instance for a specific
+(+)   Client.
+(+)     * Support.
+(+)     * Update test code adding documents to the global DocumentStore instance
+(+)       to use a local one instead.
+(+)     * Cleanup.
+(+)         * DocumentStore.open() can return the bytes object directly instead
+(+)           of having to wrap it inside a BytesIO instance.
+(+)         * Remove unnecessary Cache functions.
+(+)             * getf() should be left in the FileCache class only.
+(+)             * putf() should be removed completely.
+(+)     * Add tests.
+(+)         * Separate DocumentStore instances must not share content.
+(+)         * Not specifying a DocumentStore instance uses the default global
+(+)           one.
+(+)         * Default content.
+(+)         * Updating content.
+(+)         * Accessing existing content.
+(+)         * Accessing missing content.
+
+(17.06.2013.)
+
+(+) * (Jurko) Upgrade the setup procedure to use the latest setuptools 0.7.2
+(+)   release instead of the now deprecated 'distribute' Python package.
+(+)     * Research.
+(+)     * Implement.
+
+(18.06.2013.)
+
+(+) * (Jurko) Upgrade the setup procedure to use the latest setuptools 0.7.2
+(+)   release instead of the now deprecated 'distribute' Python package.
+(+)     * Add automated setuptools installation (downloaded on-demand from
+(+)       PyPI).
+(+)     * Fix issues with installing on Python 2.4.
+(+)     * Add project installation troubleshooting notes to the main readme.
+(+) * (Jurko) See how to allow using the setup script's 'test' option to run the
+(+)   project's pytest based test suite.
+
+(19.06.2013.)
+
+(+) * (Jurko) Resolve test failures caused by suds generating slightly different
+(+)   SOAP requests when using Python 3.3.
+(+)     * Tests should not depend on the order in which XML attributes are
+(+)       specified for a single XML element where this is not necessary.
+
+(11.11.2013.)
+
+(+) * (Jurko) Prepare the '0.4.1 jurko 5' release.
+(+)     * Follow the documented release procedure.
+(+)         * Update release notes.
+(+)         * Update version information.
+(+)         * Tag in Hg.
+(+)         * Upload the source package.
+(+)             * Project site.
+(+)             * PyPI.
+
+(18.11.2013.)
+
+(+) * (Jurko) Fix suds time-zone handling according to a pull request received
+(+)   on bitbucket from MDuggan1.
+(+)     * Research.
+(+)         * Suds assumes that all timezones have a full-hour offset from the
+(+)           UTC timeone and does not work correctly with those that do not.
+(+)             * This seems to be a suds specific problem and not a more
+(+)               general Python issue as some information on the net implies.
+(+)         * FixedOffsetTimezone.
+(+)             * datetime.tzinfo subclass.
+(+)             * Used only in test code.
+(+)             * Represents fixed offset timezones with no daylight saving
+(+)               time.
+(+)     * Start morphing the current suds & test code base towards the suggested
+(+)       patched code.
+
+(19.11.2013.)
+
+(+) * (Jurko) Fix suds time-zone handling according to a pull request received
+(+)   on bitbucket from MDuggan1.
+(+)     * Research.
+(+)     * Prepare date/time string parsing tests.
+
+(20.11.2013.)
+
+(+) * (Jurko) Start documenting the upcoming suds 0.4.1 jurko 6 release.
+(+) * (Jurko) Fix suds time-zone handling according to a pull request received
+(+)   on bitbucket from MDuggan1.
+(+)     * Research.
+(+)     * Implement parsing.
+(+)     * DateTime no longer derived from Date & Time.
+(+)     * Date constructed from datetime.datetime should hold a datetime.date.
+(+)     * Research.
+(+)         * See if sax.date.Date("1900-01-01+02:00") should hold a timezone
+(+)           aware date object.
+(+)             * Related test: TestDate.testStringToValue().
+(+)         * Timezone data.
+(+)             * See when we need to specify timezone information.
+(+)                 * In tests when we create DateTime/Time objects.
+(+)                 * Default timezone when parsing web service responses.
+(+)                     * YAGNI - for now left to user code reading specific
+(+)                       DateTime/Time objects.
+(+)                 * Default timezone when constructing web service requests.
+(+)                     * YAGNI - for now left to user code creating specific
+(+)                       DateTime/Time objects.
+(+)     * Implement.
+(+)         * Contained datetime.DateTime/Time objects should hold their
+(+)           timezone information.
+(+)     * Research.
+(+)         * Test TestDate.testStringToValue_failure() fails on Python 2 but
+(+)           passed on Python 3.
+(+)             * Fixed by the latest implementation changes.
+
+(21.11.2013.)
+
+(+) * (Jurko) Fix suds time-zone handling according to a pull request received
+(+)   on bitbucket from MDuggan1.
+(+)     * Check for feedback from users requesting this patch.
+(+)     * Add tests.
+(+)         * FixedOffsetTimezone class.
+(+)     * Fix FixedOffsetTimezone.tzname() output string formatting bug with
+(+)       negative timezone offsets.
+(+)     * Add tests.
+(+)         * UtcTimezone class.
+(+) * (Jurko) Remove support for timezone specifiers including seconds as such
+(+)   are not supported by either Python or the XSD data types specification.
+(+)     * Add/update tests.
+(+)         * Input like "+10:10:10" should be rejected.
+(+)         * Timezone offset timedelta objects containing more detailed than
+(+)           minute information.
+(+)     * Remove support.
+(+) * (Jurko) Add tests making sure timezone indicator strings without a colon
+(+)   between hours and minutes are not accepted.
+(+)     * This leads to border cases like "+121" where you do not know whether
+(+)       this represents "+01:21" or "+12:01".
+
+(22.11.2013.)
+
+(+) * (Jurko) Remove date/time related test code duplication.
+(+)     * Date, DateTime & Time classes.
+(+)         * Clean up test function names.
+(+)         * Test construction from unexpected objects.
+(+)         * Test construction from datetime.date/datetime/time objects.
+(+)         * str() tests.
+(+)     * XDate & Date class.
+(+)     * XDateTime & DateTime class.
+(+)     * XTime & Time class.
+(+)     * Timezone handling checks in XDateTime & XTime classes.
+
+(23.11.2013.)
+
+(+) * (Jurko) Make converting datetime/time to string output subsecond
+(+)   information without trailing zeroes.
+(+)     * Test.
+(+)     * Implement.
+(+)     * Discard as the implementation complexity seems way too great and the
+(+)       gain does not seem to reciprocate the cost.
+(+) * (Jurko) Update the project's versioning scheme to no longer have pip
+(+)   detect suds-jurko releases as 'prerelease' only due to our version tag
+(+)   formatting.
+(+)     * Accept that the original suds project has died and continue with the
+(+)       natural version number progression.
+(+) * (Jurko) Plan a new release.
+(+) * (Jurko) Update used setuptools version.
+(+) * (Jurko) Remove unused project files inherited from the original suds
+(+)   project.
+
+(25.11.2013.)
+
+(+) * (Jurko) Test the project with different Python installations.
+(+)     * Python 2.4.3/x86, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)             * Describe encountered problems in 'HACKING.txt'.
+(+)         * 'pytest'.
+(+)             * Describe encountered problems in 'HACKING.txt'.
+(+)         * Run tests.
+(+)     * Python 2.4.4/x86, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)             * Describe encountered problems in 'HACKING.txt'.
+(+)         * 'pytest'.
+(+)             * Describe encountered problems in 'HACKING.txt'.
+(+)         * Run tests.
+(+)     * Python 2.7.6/x64, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)         * 'pytest'.
+(+)         * Run tests.
+(+)     * Python 3.2.5/x64, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)         * 'pytest'.
+(+)         * Run tests.
+(+)     * Python 3.3.3/x86, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)         * 'pytest'.
+(+)         * Run tests.
+(+)     * Python 3.3.3/x64, on Windows 7/SP1/x64.
+(+)         * Install.
+(+)         * 'setuptools'.
+(+)         * 'pip'.
+(+)         * 'pytest'.
+(+)         * Run tests.
+(+) * (Jurko) Document the test environment setup in HACKING.txt.
+(+) * (Jurko) Prepare a new suds-jurko 0.5 release.
+
+(28.11.2013.)
+
+(+) * (Jurko) Look into a reported problem with how unicode data gets encoded
+(+)   inside a suds SOAP request with Python 2. Reported by Alexey Sveshnikov
+(+)   and mduggan1.
+(+)     * Get a reproducible use case from Alexey Sveshnikov.
+(+)     * Research.
+(+)         * Data in HTTP requests needs to be encoded as defined by the
+(+)           Content-Type header given in that request (ISO-8859-1 being the
+(+)           default).
+(+)         * Suds set the "Content-Type=text/xml; charset=utf-8" HTTP request
+(+)           header for all of its SOAP requests.
+(+)         * Python's http module (used internally by the urllib module) add
+(+)           the given message data to its existing data. Suds gives its
+(+)           message data to urllib as an utf-8 encoded bytes object. If
+(+)           existing message data is unicode, it will attempt to forcefully
+(+)           convert the given message data to unicode assuming all it contains
+(+)           is ASCII characters.
+(+)         * Suds message data is already a utf-8 encoded bytes object.
+(+)         * With Python-3 httplib's previous message data is a bytes and not a
+(+)           string object.
+(+)         * The reason why httplib's message content is converted to unicode
+(+)           is that with Python 2 the initial header is a unicode object
+(+)           u'POST /service HTTP/1.1' while with Python 3 it is a bytes object
+(+)           b'POST /service HTTP/1.1'.
+(+)             * Python 2.
+(+)                 * Affects Python 2.7.
+(+)                 * Does not affect Python 2.4.
+(+)             * Python 3.
+(+)                 * Its httplib Request object automatically converts the
+(+)                   passed URL to a bytes object (assumes it contains only
+(+)                   ASCII characters) which then prevents all the other
+(+)                   request data from being forcibly converted to unicode.
+
+(29.11.2013.)
+
+(+) * (Jurko) Look into a reported problem with how unicode data gets encoded
+(+)   inside a suds SOAP request with Python 2. Reported by Alexey Sveshnikov
+(+)   and mduggan1.
+(+)     * Reduce the reproducible use case.
+(+)         * Should not require an external web service.
+(+)         * Integrate into regular suds-jurko tests.
+(+)     * Make the reproducible test case not attempt to connect to the network
+(+)       if the test passes.
+(+)     * Fix the issue.
+(+)     * Confirm with Alexey Sveshnikov that it is ok with him to make the
+(+)       reproducible use case public.
+(+)     * Close related project pull requests on 'bitbucket.org'.
+(+) * (Jurko) Add a test for handling actual non-ASCII unicode service location
+(+)   data.
+
+(30.11.2013.)
+
+(+) * (Jurko) See if the suds HttpTransport.open() method ever gets called.
+(+)     * Yup, transport open() methods get called from DocumentReader, e.g.
+(+)       when downloading a WSDL schema from the net.
+(+)     * It seems like Transport's open() & send() methods might be megreable,
+(+)       but that would first require further research. For now - YAGNI.
+(+) * (Jurko) Process pull requests received on 'bitbucket.org'.
+(+)     * Fix setup.py current working folder path comparison so it works with
+(+)       links in the path. Contributed by ryanpetrello.
+
+(23.12.2013.)
+
+(+) * (Jurko) Prepare a basic development script for running the full suds test
+(+)   suite using multiple Python interpreter versions.
+
+(26.12.2013.)
+
+(+) * (Jurko) Process patches sent in by Bouke Haarsma on BitBucket.
+(+)     * Unicode logging issue.
+(+)         * Research.
+(+)         * Prepare tests.
+(+)         * Merge.
+(+)             * Thorough code review.
+(+)             * Implement a cleaner fix for both Reply & Request classes.
+(+)         * Update release notes.
+(+) * (Jurko) Fix possible typo in the suds.transport.Request string/unicode
+(+)   representation where there seems to be a missing space after a colon just
+(+)   before the URL information.
+(+) * (Jurko) Remove unnecessary logger objects.
+(+) * (Jurko) Process the project issue #2 reported on BitBucket by Arthur
+(+)   Clune, related to not being able to set the option cache location if the
+(+)   default cache location is not a writeable folder.
+(+)     * Research.
+(+)     * Prepare tests.
+(+)         * Default cache.
+(+)         * Default ObjectCache instance should be created only if no other
+(+)           cache has been explicitly specified during suds.client.Client()
+(+)           construction.
+(+)     * Fix.
+(+)     * Update release notes.
+(+)     * Report back & close the project issue on BitBucket.
+
+(21.01.2014.)
+
+(+) * (Jurko) Process patch sent in by Bouke Haarsma on BitBucket to 'make sure
+(+)   all web service operation parameters are consumed'.
+(+)     * This patch has been worked on on a separate feature branch for close
+(+)       to a month now.
+(+)     * Update todo list.
+(+)     * Update release notes.
+(+)     * Commit.
+(+)     * Push changes to BitBucket.
+
+(23.01.2014.)
+
+(+) * (Jurko) Prepare internal documentation notes folder.
+(+)     * Add folder & add a descriptive readme.txt file to it.
+(+)     * Include in the source distribution folder.
+(+)     * Do not include it in the installation.
+(+)     * Note the new documentation notes folder in the project's HACKING.rst
+(+)       documentation.
+
+(24.01.2014.)
+
+(+) * (Jurko) Process Jurko's research & todo notes collected while working on
+(+)   reporting extra parameter errors.
+(+) * (Jurko) Prepare a new suds-jurko 0.6 release.
+(+)     * Check release notes.
+(+)     * Test.
+(+)     * Update version tag.
+(+)     * Tag in Hg.
+(+)     * Update version information.
+
+    * (Jurko) Prepare a new suds-jurko 0.6 release.
+        * Retag in Hg
+        * Package the release.
+        * Tag next version development.
+        * Distribute the new release.
+            * PyPI.
+            * BitBucket.
+            * Notify Gauthier Bastien - see comments for commit
+              9da81de891958292850a242781b30a3493f617 on BitBucket.
+
+(25.01.2014.)
+
+    * (Jurko) Process decimal type support patch sent in by pendletongp on
+      BitBucket.
+        * Research.
+        * Prepare tests.
+        * Merge.
+        * Thorough code review.
+        * Update release notes.
+
+    * (Jurko) Clean up input argument/parameter & wrapped/unwrapped terminology
+      usage in code.
+        * Synchronize all usage both in code and in relevant 'notes/' documents.
+
+    * (Jurko) Process Jurko's remaining research & todo notes collected while
+      working on reporting extra parameter errors. Some are redundant or have
+      already been dealt with so the list needs to be triaged first.
+        * Fix non-optional choice element handling - missing values should be
+          used as empty strings, same as for non-choice parameters - search for
+          tests xfailed with reason 'non-optional choice handling buggy'.
+        * Add tests: which type XSD schemas do not get unwrapped automatically,
+          e.g. simple types such as built-in type restrictions.
+        * Split up test_request_construction.py tests into parameter definition,
+          argument parsing & possibly partial binding specific request
+          construction tests.
+        * Add test: single parameter pointing to an element - parameter name
+          should be ignored.
+        * Add test: single parameter pointing to an element - with unwrapping.
+        * Add test: single parameter pointing to a type - with unwrapping.
+        * Add test: single parameter pointing to a built-in type - no
+          unwrapping.
+        * Add test: multiple parameters pointing to types & elements.
+        * Add tests: wsdl tests - XSD schema content.
+        * Add tests: wsdl tests - recognized operations.
+        * Add tests: wsdl tests - recognized services.
+        * Add test: document/literal, document/encoded, rpc/literal &
+          rpc/encoded should make no difference to input processing in theory
+          and should all recognize the same input parameter structure.
+        * Add tests: wsdl tests - recognized binding - input/output - for
+          different methods - document binding should be the default - see
+          whether literal should be the default binding style.
+        * Add test: named sequence/choice/all - there is no such thing - their
+          name attributes should be ignored.
+        * Research: consider input parameter elements with missing type
+          declarations to have the type "xsd:string" or "xsd:any" or something
+          similar - check the XSD specification.
+            * '<xsd:element name="MyElement"></xsd:element>' currently used
+              wrapped.
+            * '<xsd:element name="MyElement"> </xsd:element>' currently used
+              unwrapped.
+        * Add test: unwrapped element reference input parameter.
+        * Add test: shema_object.resolve() &
+          shema_object.resolve(nobuiltin=True).
+        * Add test: explicitly marking an input parameter sequence as optional
+          should be recognized when unwrapping those input parameters - such
+          functions should report accepting 0 parameters.
+        * Add test: expected input argument count reporting with choice input
+          parameters explicitly marked optional.
+        * Add test: array parameters.
+        * Research: whether web service operations taking multiple input
+          parameter body-parts can have those body-parts simply reference other
+          elements, thus requiring us to resolve that reference before checking
+          whether the input parameter is optional (and possibly other places as
+          well - see Document.mkparam() which does no resolving for such web
+          service operations but does so for operations taking a single wrapper
+          input parameter structure).
+        * Research: see if the parameter type checked whether it is optional
+          inside Document.bodycontent() needs to have its ancestor elements
+          checked as well - this might be needed if this can actually be a
+          reference to some other, more deeply nested element.
+        * Research: can element references be chained - nope as they are allowed
+          to reference only top level elements and top level elements are
+          explicitly not allowed to use the ref attribute.
+        * Research: can element references be optional by themselves - yes, and
+          that is the only way they can be marked as optional since they may
+          only reference top-level elements and those are in turn not allowed to
+          use the minOccurs/maxOccurs attributes.
+        * Add test: extra array parameters - more than maxOccurs - possibly not
+          necessary if we want to allow suds to make such 'illegal' calls.
+        * Add test: missing regular parameters.
+        * Add test: missing non-optional choice parameters.
+        * Add test: missing optional choice parameters.
+        * Recognize missing optional values when specified as a lists/tuples
+          containing only values None and other such lists/tuples.
+        * Error when passed a list/tuple argument containing a list/tuple or
+          None.
+        * Research: web service operations taking multiple same-named parameters
+          - how they are and how they should be handled.
+        * Choice containing an empty sequence should be recognized as optional.
+        * Choice(a, sequence(b, c)) should set b to '' if it is not optional and
+          no value is given for it, but a value is given for c - same if b & c
+          roles are reversed.
+        * Research: suds does not seem to know how to handle minOccurs/maxOccurs
+          attributes specified on sequence & choice XSD schema XML elements - it
+          definitely does not recognize elements inside an optional choice or
+          sequence as optional - see how this affects input/output parameter
+          unwrapping.
+        * Add test: passing an array element value as an empty list.
+        * Add test: passing an array element value as a non-empty list.
+        * Add test: constructing requests with missing optional parameters -
+          should not be specified in the request.
+        * Add test: constructing requests with missing non-optional parameters
+          when the operation also takes some optional parameters - how this is
+          reported.
+        * Add test: constructing requests by using positional arguments when the
+          operation takes first optional input parameters and then non-optional
+          ones after that - initial positional arguments should be mapped to the
+          initial optional input parameters.
+        * Research: constructing requests with missing default parameters - see
+          whether this should be reported as an error or added to the request as
+          an empty value.
+        * Research: self.mkparam() call in Document.bodycontent() may return a
+          list in which case later setPrefix() method call on the returned
+          object will raise an AttributeError exception.
+        * Detecting and reporting multiple input parameter values specified for
+          a single input parameter choice group (empty choice group, simple
+          single choice, multiple independent choices, one choice group inside
+          another, one choice inside another but with additional elements
+          between them).
+        * Research: whether specifying an empty list or tuple for a parameter
+          should be treated the same as None - if so, add tests - possibly []/
+          (,) really skips the parameter while None gets mapped to an empty
+          value for non-optional parameters..
+        * Support for multiple same-named elements in a single input or output
+          structure - those directly back-to-back with each other can already be
+          set or accessed as arrays but we could need to support others as well.
+        * Constructing a request for a web service operation taking a single
+          empty all/choice/sequence parameter - should be the same as for
+          operations taking no parameters.
+        * Constructing requests for web service operations taking multiple
+          choice parameter groups (as positional and keyword arguments).
+        * Using a function with multiple choice parameter group values.
+        * TestExtraParameters.test_function_with_no_parameters() - why does
+          using WSDL '<xsd:element name="Wrapper" />' not work - seems not to be
+          recognized as wrapped data but if the XML element has any data (e.g. a
+          space or a new-line) then it gets recognized correctly - smells like
+          this could be a bug.
+        * Web service operation taking multiple input parameter bodyparts.
+            * None of which is an empty sequence.
+            * At least one of which is an empty sequence.
+        * Unrelated issues noticed while working on this.
+            * Document.document() - returned temporary can be inlined.
+            * Core.process() - root local variable seems redundant.
+            * Document.bodycontent() - mkparam() does not seem to be able to
+              return None but if it does, it might mess up tracking which
+              argument got used and we should document when it does that.
+            * Parsing parameter values (interpreting positional & keyword
+              arguments, mapping them to internal web service operation
+              parameters, reporting duplicate parameter values, reporting
+              unrecognized parameters and skipping ignored or None choice item
+              parameters) should not be binding class specific, i.e. it should
+              be shared for both document & rpc bindings.
+
+    * (Jurko) Clean up the project's basic development script for running the
+      full suds test suite using multiple Python interpreter versions.
+        * Reimplement in Python.
+        * Name better.
+
+    * (Jurko) Find a cleaner way to install suds tests. Currently they get
+      installed into a top-level 'tests' folder and so may cause conflicts with
+      some other 'tests' package that might exist in the target Python
+      environment.
+        * Ideas to consider.
+            * Placing the 'tests' folder under the 'suds' folder.
+            * Placing the top level suds folder outside the egg folder (similar
+              to how this is done for the pytest package), not having the egg
+              folder added to the Python path and leaving the tests folder
+              inside the egg folder.
+
+NON PRIORETIZED:
+=================================================
+
+    * Optionally make suds check that the input parameters passed to its web
+      service proxy operations actually match their respective WSDL definitions.
+        * Current suds behaviour.
+            * Extra parameters are ignored.
+            * Parameters of invalid type are simply added into the generated
+              SOAP request as strings and left up to the web service
+              implementation to deal with and report as an error if the
+              constructed SOAP request turns out to be invalid.
+        * Having a local client-side check could make catching client side
+          programming bugs easier.
+        * Ideas.
+            * Make sure given values fit their respective type value domains,
+              e.g. integers, strings, regular expressions, restrictions, complex
+              types where a builtin was expected, complex type of an unexpected
+              structure, etc.
+            * Extra parameters are reported as errors.
+            * Missing non-optional parameters are reported as errors.
+            * Input message part has both element & type specified.
+
+    * See how invalid schemas containing a ref-cycle are handled.
+        * They should be reported as invalid either when dereferencing them
+          (e.g. to determine an element's target namespace) or when building the
+          internal schema object tree and should not cause us to go into endless
+          recursion.
+
+    * See how multi-occurrence input parameter elements are supposed to be
+      supported.
+        * With automated parameter unwrapping support.
+        * Without automated parameter unwrapping support.
+
+    * Clean up & correct the choice support implementation.
+        * Choice parameters seem to be supported only for document/literal style
+          input parameters only.
+            * Add tests for this.
+            * Fix.
+        * See the currently disabled
+          'xxxtest_choice_parameter_implementation_inconsistencies' unit test
+          demonstrating a problem with the current implementation.
+
+    * SoapClient 'location' cleanup.
+        * URL quoting, especially if specified externally by the caller instead
+          of having been read from a valid WSDL schema.
+
+    * See how input parameter element for a document/literal web service
+      operation style gets handled when that element has 2 occurrences.
+        * See how this affects suds library's automated input data unwrapping.
+        * See how this sort of data is supposed to be entered anyway.
+
+    * FileCache class fixes.
+        * Remove incorrectly created cache files, e.g. if it gets created but
+          then writing to it fails.
+        * Make sure reading a cache file does not crash the whole Python
+          interpreter process.
+            * Encountered when trying to read an empty cache file using a debug
+              Python version 3.2.3 on Windows 7 - triggered an internal Python
+              assertion failure.
+
+    * Look into the 'suds.null' class.
+        * Research.
+            * See sources (grep for 'null'), old release notes (README.txt) and
+              commit messages.
+            * What it is for.
+            * Whether it can be replaced with None.
+            * If it serves some purpose see if it should be used for identifying
+              missing 'choice' structure members as well.
+
+    * Make it simpler to run Py3 tests.
+        * Current status.
+            * May be run without additional pytest options by running 'setup.py
+              test' from the root project folder.
+                * This method will automatically download & install pytest from
+                  PyPI if needed, but will leave them installed in the project's
+                  root folder.
+            * May be run from the root project folder by first building the
+              project to a temporary location: 'setup.py build & pytest build'.
+                * This method allows specifying additional pytest command-line
+                  arguments.
+        * Do some more thinking on this and, when done, update related HACKING
+          notes.
+
+    * Generate suds Python library documentation (epydoc).
+        * Research.
+            * HTML.
+            * PDF.
+        * Decide how to maintain, generate & distribute this documentation.
+        * Update project HACKING notes to note the external software required
+          for generating the documentation.
+        * Update release procedure to include releasing the documentation.
+        * Once the official project documentation has been integrated into this
+          fork, go through existing internal project notes in our fork and see
+          how to integrate them into the official project documentation.
+
+    * Research.
+        * Test how optional elements under a choice work.
+            * There are some comments & an additional patch related to this at
+              'https://fedorahosted.org/suds/ticket/342'.
+        * Default element values.
+            * What they actually mean.
+                * From from 'http://www.w3.org/TR/xmlschema-0'.
+                    * If the element does not appear it is not provided; if it
+                      does appear and it is empty, its value is the specified
+                      default value; otherwise its value is that given.
+            * How elements with default values inside a choice structure should
+              be handled.
+        * See what the suds.sudsobjects.Facade class is for.
+        * How to implement test cases requiring a test web service.
+        * See how to connect to a web service via a Proxy server requiring NTLM
+          authentication.
+            * There are some projects seen on the net implementing a NTLM
+              authentication handler for urllib.
+            * Testing this will require implementing a proxy server requiring
+              NTLM authentication and a web service or at least a web service
+              requiring NTLM authentication.
+        * Using pylint.
+
+    * Implement an urllib connection handler allowing connecting using HTTPS
+      with client authentication.
+        * Prepare a test (will require a test web service).
+        * Implement.
+
+    * Prepare additional test cases.
+        * Loading a wsdl from a file as in suds.client.Client(
+          "file://localhost/Folder/aaa.wsdl").
+            * Path not containing spaces.
+            * Path containing spaces.
+        * Invalid element reference.
+        * Handing schema imports.
+            * With the same target namespace.
+            * With a different target namespace.
+        * Prepared SOAP operation invocation requests.
+            * With choice parameters.
+        * Calling a web service operation with no parameters.
+            * RPC binding style.
+            * Document binding style.
+
+    * Process ideas collected from external projects using suds.
+        * Alternative choice implementation that would not automatically expand
+          all choice function parameters but instead take some more generic
+          'choice' object parameter.
+            * This object would then know which of its data members is
+              'currently specified'.
+        * See whether the Marshaller class should know about choice elements and
+          not add XML nodes for elements with the value 'None' if they are
+          contained directly inside a choice.
+            * For some more background information on this see Marshaller
+              related release notes & commit messages inherited from the
+              original 'suds' development project.
+        * See if the class 'suds.xsd.sxbasic.Complex' function sequence() should
+          return True if its only child is a sequence node.
+        * Typo corrections for the original project web site.
+            * 'docuemnt' --> 'document'.
+            * 'becuase' --> 'because'.
+
+    * See whether standard and/or suds supports specifying message part
+      parameter element directly from an external schema.
+        * For example, 'integer' or 'string' elements defined in the
+          'http://schemas.xmlsoap.org/soap/encoding/' namespace (stored
+          internally by suds in the store.py module).
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 0000000..72d35a5
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,382 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import os
+import shutil
+import sys
+import tempfile
+import tarfile
+import optparse
+import subprocess
+import platform
+
+from distutils import log
+
+try:
+    from site import USER_SITE
+except ImportError:
+    USER_SITE = None
+
+DEFAULT_VERSION = "1.4"
+DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
+
+def _python_cmd(*args):
+    args = (sys.executable,) + args
+    return subprocess.call(args) == 0
+
+def _check_call_py24(cmd, *args, **kwargs):
+    res = subprocess.call(cmd, *args, **kwargs)
+    class CalledProcessError(Exception):
+        pass
+    if not res == 0:
+        msg = "Command '%s' return non-zero exit status %d" % (cmd, res)
+        raise CalledProcessError(msg)
+vars(subprocess).setdefault('check_call', _check_call_py24)
+
+def _install(tarball, install_args=()):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # installing
+        log.warn('Installing Setuptools')
+        if not _python_cmd('setup.py', 'install', *install_args):
+            log.warn('Something went wrong during the installation.')
+            log.warn('See the error message above.')
+            # exitcode will be 2
+            return 2
+    finally:
+        os.chdir(old_wd)
+        shutil.rmtree(tmpdir)
+
+
+def _build_egg(egg, tarball, to_dir):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # building an egg
+        log.warn('Building a Setuptools egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+
+    finally:
+        os.chdir(old_wd)
+        shutil.rmtree(tmpdir)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+def _do_download(version, download_base, to_dir, download_delay):
+    egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
+                       % (version, sys.version_info[0], sys.version_info[1]))
+    if not os.path.exists(egg):
+        tarball = download_setuptools(version, download_base,
+                                      to_dir, download_delay)
+        _build_egg(egg, tarball, to_dir)
+    sys.path.insert(0, egg)
+
+    # Remove previously-imported pkg_resources if present (see
+    # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
+    if 'pkg_resources' in sys.modules:
+        del sys.modules['pkg_resources']
+
+    import setuptools
+    setuptools.bootstrap_install_from = egg
+
+
+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                   to_dir=os.curdir, download_delay=15):
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    was_imported = 'pkg_resources' in sys.modules or \
+        'setuptools' in sys.modules
+    try:
+        import pkg_resources
+    except ImportError:
+        return _do_download(version, download_base, to_dir, download_delay)
+    try:
+        pkg_resources.require("setuptools>=" + version)
+        return
+    except pkg_resources.VersionConflict:
+        e = sys.exc_info()[1]
+        if was_imported:
+            sys.stderr.write(
+            "The required version of setuptools (>=%s) is not available,\n"
+            "and can't be installed while this script is running. Please\n"
+            "install a more recent version first, using\n"
+            "'easy_install -U setuptools'."
+            "\n\n(Currently using %r)\n" % (version, e.args[0]))
+            sys.exit(2)
+        else:
+            del pkg_resources, sys.modules['pkg_resources']    # reload ok
+            return _do_download(version, download_base, to_dir,
+                                download_delay)
+    except pkg_resources.DistributionNotFound:
+        return _do_download(version, download_base, to_dir,
+                            download_delay)
+
+def _clean_check(cmd, target):
+    """
+    Run the command to download target. If the command fails, clean up before
+    re-raising the error.
+    """
+    try:
+        subprocess.check_call(cmd)
+    except subprocess.CalledProcessError:
+        if os.access(target, os.F_OK):
+            os.unlink(target)
+        raise
+
+def download_file_powershell(url, target):
+    """
+    Download the file at url to target using Powershell (which will validate
+    trust). Raise an exception if the command cannot complete.
+    """
+    target = os.path.abspath(target)
+    cmd = [
+        'powershell',
+        '-Command',
+        "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
+    ]
+    _clean_check(cmd, target)
+
+def has_powershell():
+    if platform.system() != 'Windows':
+        return False
+    cmd = ['powershell', '-Command', 'echo test']
+    devnull = open(os.path.devnull, 'wb')
+    try:
+        try:
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
+        except:
+            return False
+    finally:
+        devnull.close()
+    return True
+
+download_file_powershell.viable = has_powershell
+
+def download_file_curl(url, target):
+    cmd = ['curl', url, '--silent', '--output', target]
+    _clean_check(cmd, target)
+
+def has_curl():
+    cmd = ['curl', '--version']
+    devnull = open(os.path.devnull, 'wb')
+    try:
+        try:
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
+        except:
+            return False
+    finally:
+        devnull.close()
+    return True
+
+download_file_curl.viable = has_curl
+
+def download_file_wget(url, target):
+    cmd = ['wget', url, '--quiet', '--output-document', target]
+    _clean_check(cmd, target)
+
+def has_wget():
+    cmd = ['wget', '--version']
+    devnull = open(os.path.devnull, 'wb')
+    try:
+        try:
+            subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
+        except:
+            return False
+    finally:
+        devnull.close()
+    return True
+
+download_file_wget.viable = has_wget
+
+def download_file_insecure(url, target):
+    """
+    Use Python to download the file, even though it cannot authenticate the
+    connection.
+    """
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+    src = dst = None
+    try:
+        src = urlopen(url)
+        # Read/write all in one block, so we don't create a corrupt file
+        # if the download is interrupted.
+        data = src.read()
+        dst = open(target, "wb")
+        dst.write(data)
+    finally:
+        if src:
+            src.close()
+        if dst:
+            dst.close()
+
+download_file_insecure.viable = lambda: True
+
+def get_best_downloader():
+    downloaders = [
+        download_file_powershell,
+        download_file_curl,
+        download_file_wget,
+        download_file_insecure,
+    ]
+
+    for dl in downloaders:
+        if dl.viable():
+            return dl
+
+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                        to_dir=os.curdir, delay=15,
+                        downloader_factory=get_best_downloader):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download
+    attempt.
+
+    ``downloader_factory`` should be a function taking no arguments and
+    returning a function for downloading a URL to a target.
+    """
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    tgz_name = "setuptools-%s.tar.gz" % version
+    url = download_base + tgz_name
+    saveto = os.path.join(to_dir, tgz_name)
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        log.warn("Downloading %s", url)
+        downloader = downloader_factory()
+        downloader(url, saveto)
+    return os.path.realpath(saveto)
+
+
+def _extractall(self, path=".", members=None):
+    """Extract all members from the archive to the current working
+       directory and set owner, modification time and permissions on
+       directories afterwards. `path' specifies a different directory
+       to extract to. `members' is optional and must be a subset of the
+       list returned by getmembers().
+    """
+    import copy
+    import operator
+    from tarfile import ExtractError
+    directories = []
+
+    if members is None:
+        members = self
+
+    for tarinfo in members:
+        if tarinfo.isdir():
+            # Extract directories with a safe mode.
+            directories.append(tarinfo)
+            tarinfo = copy.copy(tarinfo)
+            tarinfo.mode = 448  # decimal for oct 0700
+        self.extract(tarinfo, path)
+
+    # Reverse sort directories.
+    if sys.version_info < (2, 4):
+        def sorter(dir1, dir2):
+            return cmp(dir1.name, dir2.name)
+        directories.sort(sorter)
+        directories.reverse()
+    else:
+        directories.sort(key=operator.attrgetter('name'), reverse=True)
+
+    # Set correct owner, mtime and filemode on directories.
+    for tarinfo in directories:
+        dirpath = os.path.join(path, tarinfo.name)
+        try:
+            self.chown(tarinfo, dirpath)
+            self.utime(tarinfo, dirpath)
+            self.chmod(tarinfo, dirpath)
+        except ExtractError:
+            e = sys.exc_info()[1]
+            if self.errorlevel > 1:
+                raise
+            else:
+                self._dbg(1, "tarfile: %s" % e)
+
+
+def _build_install_args(options):
+    """
+    Build the arguments to 'python setup.py install' on the setuptools package
+    """
+    install_args = []
+    if options.user_install:
+        if sys.version_info < (2, 6):
+            log.warn("--user requires Python 2.6 or later")
+            raise SystemExit(1)
+        install_args.append('--user')
+    return install_args
+
+def _parse_args():
+    """
+    Parse the command line for options
+    """
+    parser = optparse.OptionParser()
+    parser.add_option(
+        '--user', dest='user_install', action='store_true', default=False,
+        help='install in user site package (requires Python 2.6 or later)')
+    parser.add_option(
+        '--download-base', dest='download_base', metavar="URL",
+        default=DEFAULT_URL,
+        help='alternative URL from where to download the setuptools package')
+    parser.add_option(
+        '--insecure', dest='downloader_factory', action='store_const',
+        const=lambda: download_file_insecure, default=get_best_downloader,
+        help='Use internal, non-validating downloader'
+    )
+    options, args = parser.parse_args()
+    # positional arguments are ignored
+    return options
+
+def main(version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    options = _parse_args()
+    tarball = download_setuptools(download_base=options.download_base,
+        downloader_factory=options.downloader_factory)
+    return _install(tarball, _build_install_args(options))
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/makefile b/makefile
deleted file mode 100644
index 32a3f9e..0000000
--- a/makefile
+++ /dev/null
@@ -1,71 +0,0 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-#
-
-PKG = python-suds
-SPEC = $(PKG).spec
-SETUP = setup.py
-DOCTAR = suds-docs.tar.gz
-FEDORAPEOPLE = jortel at fedorapeople.org
-
-all : rpm docs
-
-dist : clean
-	mkdir dist
-	./sdist
-	./sdist python
-
-rpm : dist
-	cp dist/$(PKG)*.gz /usr/src/redhat/SOURCES
-	rpmbuild -ba $(SPEC)
-	cp /usr/src/redhat/RPMS/noarch/$(PKG)*.rpm dist
-	cp /usr/src/redhat/SRPMS/$(PKG)*.rpm dist
-	rpmlint -i dist/$(PKG)*.rpm
-
-release : rpm rdocs
-	scp dist/python*.tar.gz fedorahosted.org:suds
-	scp dist/python*.rpm fedorahosted.org:suds
-
-register :
-	python setup.py sdist bdist_egg register upload
-
-rdocs : docs
-	scp /tmp/$(DOCTAR) $(FEDORAPEOPLE):
-	ssh $(FEDORAPEOPLE) 'cd public_html/suds; rm -rf doc; tar xmzvf ~/$(DOCTAR)'
-
-docs :
-	rm -rf doc
-	rm -f /tmp/$(DOCTAR)
-	epydoc -vo doc `find suds -name "*.py"`
-	tar czvf /tmp/$(DOCTAR) doc
-
-pdf :
-	epydoc -vo doc --pdf `find suds -name \*.py`
-	mv doc/api.pdf doc/sudsapi.pdf
-
-clean :
-	rm -rf dist
-	rm -rf build
-	rm -rf doc
-	rm -rf *.egg-info
-	rm -rf /usr/src/redhat/BUILD/$(PKG)*
-	rm -rf /usr/src/redhat/RPMS/noarch/$(PKG)*
-	rm -rf /usr/src/redhat/SOURCES/$(PKG)*
-	rm -rf /usr/src/redhat/SRPMS/$(PKG)*
-	find . -name "*.pyc" -exec rm -f {} \;
-	find . -name "*~" -exec rm -f {} \;
-
-.PHONY : clean register docs pdf
diff --git a/notes/argument_parsing.rst b/notes/argument_parsing.rst
new file mode 100644
index 0000000..f945381
--- /dev/null
+++ b/notes/argument_parsing.rst
@@ -0,0 +1,157 @@
+===================================================================
+Notes on input argument usage when invoking a web service operation
+===================================================================
+:Authors: Jurko Gospodnetiæ
+:Date: 2014-01-23
+
+
+===========
+Definitions
+===========
+
+message parts
+  WSDL schema defines message parts representing a web service operation's input
+  data. Each such part's data structure is defined by mapping the message part
+  to XSD element or type.
+
+input parameters
+  Suds library's view of a web service operation's input data. Each message part
+  may correspond to 1 or more input parameters. See the `Bare/wrapped web
+  service operation input parameters`_ section for more detailed information.
+
+input arguments
+  Values passed to suds library's Python function or function object invoking a
+  web service operation. Suds attempts to map each input argument to a single
+  web service operation input parameter.
+
+
+===================================================
+Bare/wrapped web service operation input parameters
+===================================================
+
+When suds library models a web service operation, it can be configured to map
+each of its message parts to a single input parameter expecting values directly
+matching the data type defined for that message part in the web service's WSDL
+schema. Such input parameters are called ``bare``.
+
+If a particular message part has a type which is actually a structure containing
+a collection of simpler elements, then suds may be configured to map each of
+those simpler elements to a single input parameter. Such input parameters are
+called 'wrapped'.
+
+If an input parameter is represented by structured XSD element containing other
+elements, suds may treat it as either ``bare`` or ``wrapped``. If it is
+considered ``wrapped``, then suds library's web service operation invocation
+function will take values for the input parameter's internal elements as input
+arguments, instead of taking only a single wrapper object as a value for the
+external wrapper element.
+
+``wrapped`` input parameter support has been implemented to make the interface
+simpler for the user/programmer using suds to invoke web service operations. It
+does not affect how the the resulting web service operation invocation request
+is constructed, i.e. passing a suds object as a single ``bare`` input parameter
+value, or passing matching contained element values as separate wrapped input
+parameter values results in the same web service operation invocation request
+being constructed.
+
+Example:
+--------
+
+Consider an operation taking the following element as its only message part::
+
+  <xsd:element name="unga">
+    <sequence>
+      <xsd:element name="a" type="xsd:string"/>
+      <xsd:element name="b" type="xsd:integer"/>
+      <xsd:element name="c" type="MyType"/>
+    </xsd:sequence>
+  </xsd:element>
+
+Suds may be configured to map that message part into with a single ``bare`` or
+three ``wrapped`` input parameters.
+
+If a ``bare`` input parameter is used, the operation invocation function would
+take only a single argument:
+
+* a suds object argument for element ``unga``
+
+If ``wrapped`` input parameters are used, the operation invocation function
+would take the following input arguments:
+
+* a string argument for element ``a``
+* an integer argument for element ``b``
+* a suds object argument for element ``c``
+
+
+==================================================================
+Input parameter values in original and current suds implementation
+==================================================================
+
+* A user may or may not specify a value for a specific input parameter.
+* We refer to unspecified or ``None`` input parameter values as `undefined`.
+* We refer to all other input parameter values `defined`.
+
+Original suds library implementation:
+-------------------------------------
+
+* An element may be explicitly marked as optional.
+* ``choice`` input parameter structures not supported.
+* A defined input parameter value is used directly.
+* An undefined optional input parameter value is ignored.
+* An undefined non-optional input parameter value is interpreted as an empty
+  string.
+* Multiple values specified for a single input parameter are ignored.
+* Extra input arguments are ignored.
+
+Defects:
+
+* ``choice`` input parameter structures not supported correctly. When used,
+  result in incorrectly constructed web service operation invocation requests.
+* An ``all``/``choice``/``sequence`` input parameter structure may be explicitly
+  marked as optional, but this is ignored when deciding whether a specific input
+  parameter inside that structure is optional or not.
+* No error when multiple values are specified for a single input parameter.
+* No error on extra input arguments.
+
+Current suds library implementation:
+------------------------------------
+
+* An element may be explicitly marked as optional.
+* ``choice`` input parameter structures supported.
+* Input parameters contained inside a ``choice`` input parameter structure
+  (either directly or indirectly) are always considered optional.
+* An input parameter structure containing at least one input parameter with a
+  defined value (either directly or indirectly) is considered to have a defined
+  value.
+* A defined input parameter value is used directly.
+* An undefined optional input parameter value is ignored.
+* An undefined non-optional input parameters value is treated as an empty
+  string.
+
+Configurable features:
+
+* Multiple values specified for a single input parameter may be reported as an
+  error.
+* ``choice`` input parameter structure directly containing multiple input
+  parameters and/or input parameter structures with defined values may be
+  reported as an error.
+* Extra input argument may be reported as an error.
+
+Defects (demonstrated by existing unit tests):
+
+* An ``all``/``choice``/``sequence`` input parameter structure may be explicitly
+  marked as optional, but this is ignored when deciding whether a specific input
+  parameter inside that structure is optional or not.
+* Undefined value for a non-optional input parameter contained directly inside
+  an ``all``/``sequence`` input parameter structure contained inside a
+  ``choice`` input parameter structure should be treated as an empty string if
+  the ``all``/``sequence`` input parameter structure has a defined value.
+* A ``choice`` input parameter structure directly containing an input parameter
+  structure with no elements should be considered optional.
+
+Still missing features:
+-----------------------
+
+* Non-optional ``choice`` input parameter structure with no defined value should
+  be treated as if its first input parameter's value had been specified as an
+  empty string.
diff --git a/notes/readme.txt b/notes/readme.txt
new file mode 100644
index 0000000..1ce3672
--- /dev/null
+++ b/notes/readme.txt
@@ -0,0 +1,9 @@
+This documentation has been started because we needed some place to put our
+design, research & development related notes collected collected while hacking
+on this project. The official suds project documentation has not been included
+in this fork, as it was not stored in the same repository as the original
+project's code base.
+
+Once official suds project documentation has been integrated into this fork,
+parts of this documentation that start getting some well rounded form, should be
+integrated into the official project documentation.
diff --git a/notes/traversing_client_data.rst b/notes/traversing_client_data.rst
new file mode 100644
index 0000000..d06f5f6
--- /dev/null
+++ b/notes/traversing_client_data.rst
@@ -0,0 +1,28 @@
+================================================
+Examples on traversing suds library's data model
+================================================
+:Authors: Jurko Gospodnetiæ
+:Date: 2014-01-23
+
+Get service from client::
+
+  service = client.service
+
+Get client from service (for debugging purposes only)::
+
+  client = service._ServiceSelector__client
+
+Get XSD schema information from client::
+
+  schema = client.wsdl.schema
+  schema.root      # root schema XML element
+  schema.all       # all of the schema's imported direct child objects (model)
+  schema.children  # all of the schema's direct child objects (model)
+  schema.elements  # (name, namespace) --> top level element mapping (model)
+  schema.types     # (name, namespace) --> top level type mapping (model)
+
+
+Get XSD schema model object's direct children (i.e. elements, sequences,
+choices, etc.)::
+
+  schema_object.rawchildren
diff --git a/python-suds.spec b/python-suds.spec
deleted file mode 100644
index 8d40ac0..0000000
--- a/python-suds.spec
+++ /dev/null
@@ -1,213 +0,0 @@
-%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
-
-Summary: A python SOAP client
-Name:  python-suds
-Version: 0.4.1
-Release: 1%{?dist}
-Source0: https://fedorahosted.org/releases/s/u/suds/%{name}-%{version}.tar.gz
-License: LGPLv3+
-Group: Development/Libraries
-BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
-BuildArch: noarch
-Requires: python >= 2.4
-BuildRequires: python-setuptools-devel
-Url: https://fedorahosted.org/suds
-
-%description
-The suds project is a python soap web services client lib.  Suds leverages
-python meta programming to provide an intuitive API for consuming web
-services.  Objectification of types defined in the WSDL is provided
-without class generation.  Programmers rarely need to read the WSDL since
-services and WSDL based objects can be easily inspected.
-
-%prep
-%setup -q
-
-%build
-python setup.py sdist
-
-%install
-rm -rf $RPM_BUILD_ROOT
-python setup.py install --optimize=1 --root=$RPM_BUILD_ROOT
-
-%clean
-rm -rf $RPM_BUILD_ROOT
-
-%files
-%defattr(-,root,root,-)
-%{python_sitelib}/suds*.egg-info
-%dir %{python_sitelib}/suds
-%dir %{python_sitelib}/suds/bindings
-%dir %{python_sitelib}/suds/sax
-%dir %{python_sitelib}/suds/xsd
-%dir %{python_sitelib}/suds/mx
-%dir %{python_sitelib}/suds/umx
-%dir %{python_sitelib}/suds/transport
-%{python_sitelib}/suds/*.py*
-%{python_sitelib}/suds/bindings/*.py*
-%{python_sitelib}/suds/sax/*.py*
-%{python_sitelib}/suds/xsd/*.py*
-%{python_sitelib}/suds/mx/*.py*
-%{python_sitelib}/suds/umx/*.py*
-%{python_sitelib}/suds/transport/*.py*
-
-%doc README LICENSE
-
-%changelog
-* Thu Oct 15 2010 jortel <jortel at redhat.com> - 0.4.1-1
-- 0.4.1
-
-* Thu Sep 8 2010 jortel <jortel at redhat.com> - 0.4-1
-- Fix spelling errors in spec description.
-- Fix source0 URL warning.
-- Updated caching to not cache intermediate wsdls.
-- Added DocumentCache which caches verified XML documents as text. User can choose.
-- Added cachingpolicy option to allow user to specify whether to cache XML documents or the WSDL object.
-- Provided for repeating values in reply for message parts consistent with way handled in nested objects.
-- Added charset=utf-8 to stock content-type http header.
-- Added <?xml version="1.0" encoding="UTF-8"?> to outgoing SOAP messages.
-- Detection of faults in successful (http=200) replies and raise WebFault. Search for <soapenv:Fault/>.
-- Add plugins facility. 
-- Fixed Tickets: #251, #313, #314, #334
-
-* Thu Jul 22 2010 David Malcolm <dmalcolm at redhat.com> - 0.3.9-2
-- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild
-
-* Thu Dec 17 2009 jortel <jortel at redhat.com> - 0.3.9-1
-- Bumped python requires to 2.4
-- Replaced stream-based caching in the transport package with document-based caching.
-- Caches pickled Document objects instead of XML text. 2x Faster!
-- No more SAX parsing exceptions on damaged or incomplete cached files. 
-- Cached WSDL objects. Entire Definitions object including contained Schema object cached via pickle.
-- Copy of soap encoding schema packaged with suds.
-- Refactor Transports to use ProxyHandler instead of urllib2.Request.set_proxy().
-- Added WSSE enhancements <Timestamp/> and <Expires/> support. See: Timestamp token. 
-- Fixed Tickets: #256, #291, #294, #295, #296
-
-* Wed Dec 9 2009 jortel <jortel at redhat.com> - 0.3.8-1
-- Includeds Windows NTLM Transport.
-- Add missing self.messages in Client.clone().
-- Changed default behavior for WSDL PartElement to be optional.
-- Add support for services/ports defined without <address/> element in WSDL.
-- Fix sax.attribute.Element.attrib() to find by name only when ns is not specified; renamed to Element.getAttribute().
-- Update HttpTransport to pass timeout parameter to urllib2 open() methods when supported by urllib2.
-- Add null class to pass explicit NULL values for parameters and optional elements.
-- Soap encoded array (soap-enc:Array) enhancement for rpc/encoded.
-  Arrays passed as python arrays - works like document/literal now.
-  No more using the factory to create the Array.
-  Automatically includes arrayType attribute.  Eg: soap-enc:arrayType="Array[2]".
-  Reintroduced ability to pass complex (objects) using python dict instead of suds object via factory.
-- Fixed tickets: #84, #261, #262, #263, #265, #266, #278, #280, #282.
-
-* Thu Oct 16 2009 jortel <jortel at redhat.com> - 0.3.7-1
-- Better soap header support
-- Added new transport HttpAuthenticated for active (not passive) basic authentication.
-- New options (prefixes, timeout, retxml)
-- WSDL processing enhancements.
-- Expanded builtin XSD type support.
-- Fixed <xs:iniclude/>
-- Better XML date/datetime conversion.
-- Client.clone() method added for lightweight copy of client object.
-- XSD processing fixes/enhancements.
-- Better <simpleType/> by <xs:restriction/> support.
-- Performance enhancements. 
-- Fixed tickets: #65, #232, #233, #235, #241, #242, #244, #247, #254, #254, #256, #257, #258
-
-* Sun Jul 26 2009 Fedora Release Engineering <rel-eng at lists.fedoraproject.org> - 0.3.6-2
-- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild
-
-* Wed May 1 2009 jortel <jortel at redhat.com> - 0.3.6-1
-- Change hard coded /tmp/suds to tempfile.gettempdir() and create suds/ on demand.
-- Fix return type for Any.get_attribute().
-- Update http caching to ignore file:// urls.
-- Better logging of messages when only the reply is injected.
-- Fix XInteger and XFloat types to translate returned arrays properly.
-- Fix xs:import schema with same namespace.
-- Update parser to not load external references and add Import.bind() for XMLSchema.xsd location.
-- Add schema doctor - used to patch XSDs at runtime.  (See Options.doctor)
-- Fix deprecation warnings in python 2.6.
-- Add behavior for @default defined on <element/>.
-- Change @xsi:type value to always be qualified for doc/literal.
-- Add Option.xstq option to control when @xsi:type is qualified.
-- Fixed Tickets: #64, #129, #205, #206, #217, #221, #222, #224, #225, #228, #229, #230
-
-* Wed Feb 25 2009 jortel <jortel at redhat.com> - 0.3.5-1
-- Adds http caching.  Default is (1) day.
-- Removed checking fc version in spec since no longer building < fc9.
-- Updated makefile to roll tarball with tar.sh.
-- Moved bare/wrapped determination to wsdl for document/literal.
-- Refactored Transport to provide better visibility into http headers.
-- Fixed Tickets: #207, #207, #209, #210, #212, #214, #215
-
-* Mon Dec 08 2008 jortel <jortel at redhat.com> - 0.3.4-1
-- Static (automatic) Import.bind('http://schemas.xmlsoap.org/soap/encoding/')
-- Basic ws-security with {{{UsernameToken}}} and clear-text password only.
-- Add support for ''sparse'' soap headers via passing dictionary
-- Add support for arbitrary user defined soap headers
-- Fixes service operations with multiple soap header entries.
-- Schema loading and dereferencing algorithm enhancements.
-- Nested soap multirefs fixed.
-- Better (true) support for elementFormDefault="unqualified" provides more accurate namespaing.
-- WSDL part types no longer default to WSDL targetNamespace.
-- Fixed Tickets: #4, #6, #21, #32, #62, #66, #71, #72, #114, #155, #201.
-
-* Wed Dec 04 2008 jortel <jortel at redhat.com> - 0.3.3-2
-- Rebuild for Python 2.6
-
-* Wed Dec 04 2008 jortel <jortel at redhat.com> - 0.3.3-1
-- No longer installs (tests) package.
-- Implements API-3 proposal
-    Pluggable transport
-    Keyword method arguments
-    Baisc http authentication in default transport
-- Add namespace prefix normalization in soap message.
-- Better soap message pruning of empty nodes.
-- Fixed Tickets: #51 - #60.
-
-* Sat Nov 29 2008 Ignacio Vazquez-Abrams <ivazqueznet+rpm at gmail.com> - 0.3.2-2
-- Rebuild for Python 2.6
-
-* Fri Nov 06 2008 jortel <jortel at redhat.com> - 0.3.2-1
-- Add SOAP MultiRef support
-- Add support for new schema tags:
-    <xs:include/>
-    <xs:simpleContent/>
-    <xs:group/>
-    <xs:attributeGroup/>
-- Added support for new xs <--> python type conversions:
-    xs:int
-    xs:long
-    xs:float
-    xs:double
-- Revise marshaller and binding to further sharpen the namespacing of nodes produced.
-- Infinite recursion fixed in ''xsd'' package dereference() during schema loading.
-- Add support for <wsdl:import/> of schema files into the wsdl root <definitions/>.
-- Fix double encoding of (&)
-- Add Client API:
-    setheaders()  - Same as keyword but works for all invocations.
-    addprefix()   - Mapping of namespace prefixes.
-    setlocation() - Override the location in the wsdl.
-    setproxy()    - Same as proxy keyword but for all invocations.
-- Add proper namespace prefix for soap headers.
-- Fixed Tickets: #5, #12, #34, #37, #40, #44, #45, #46, #48, #49, #50, #51
-
-* Fri Nov 03 2008 jortel <jortel at redhat.com> - 0.3.1-5
-- Add LICENSE to %%doc.
-
-* Fri Oct 28 2008 jortel <jortel at redhat.com> - 0.3.1-4
-- Changes acc. #466496 Comment #8
-
-* Fri Oct 27 2008 jortel <jortel at redhat.com> - 0.3.1-3
-- Add "rm -rf $RPM_BUILD_ROOT" to install
-
-* Fri Oct 16 2008 jortel <jortel at redhat.com> - 0.3.1-2
-- Changes acc. #466496 Comment #1
-
-* Fri Oct 10 2008 jortel <jortel at redhat.com> - 0.3.1-1
-- Extends the support for multi-port services introduced earlier. This addition, 
-  provides for multiple services to define the *same* method and suds will
-  handle it properly.  See section 'SERVICES WITH MULTIPLE PORTS:'
-- Add support for multi-document document/literal soap binding style.
-  See section 'MULTI-DOCUMENT Docuemnt/Literal:'
-- Add support for (xs:group, xs:attributeGroup) tags.
-- Add Client.last_sent() and Client.last_received().
diff --git a/setup.cfg b/setup.cfg
index 2dca2f8..36ee378 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,14 @@
-[install]
-optimize = 1
+[install]
+optimize = 1
+
+[sdist]
+formats = bztar,zip
+
+[pytest]
+norecursedirs = .git .hg .svn build dist
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
index 659e123..12bc3af 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -16,18 +16,206 @@
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
-import sys
-import suds
+# Automatically download & install an appropriate setuptools version if needed.
+import ez_setup
+ez_setup.use_setuptools()
+
+# 'setuptools' related packages.
+import pkg_resources
 from setuptools import setup, find_packages
 
+import os
+import os.path
+import sys
+
+
+def read_python_code(filename):
+    "Returns the given Python source file's compiled content."
+    file = open(filename, "rt")
+    try:
+        source = file.read()
+    finally:
+        file.close()
+    #   Python 2.6 and below did not support passing strings to exec() &
+    # compile() functions containing line separators other than '\n'. To
+    # support them we need to manually make sure such line endings get
+    # converted even on platforms where this is not handled by native text file
+    # read operations.
+    source = source.replace("\r\n", "\n").replace("\r", "\n")
+    return compile(source, filename, "exec")
+
+
+# Setup documentation incorrectly states that it will search for packages
+# relative to the setup script folder by default when in fact it will search
+# for them relative to the current working folder. It seems avoiding this
+# problem cleanly and making the setup script runnable with any current working
+# folder would require better setup() support.
+# Attempted alternatives:
+#   * Changing the current working folder internally makes any passed path
+#     parameters be interpreted relative to the setup script folder when they
+#     should be interpreted relative to the initial current working folder.
+#   * Passing the script folder as setup() & find_packages() function
+#     parameters makes the final installed distribution contain the absolute
+#     package source location information and not include some other meta-data
+#     package information as well.
+script_folder = os.path.realpath(os.path.dirname(__file__))
+current_folder = os.path.realpath(os.getcwd())
+if script_folder != current_folder:
+    print("ERROR: Suds library setup script needs to be run from the folder "
+        "containing it.")
+    print()
+    print("Current folder: %s" % current_folder)
+    print("Script folder: %s" % script_folder)
+    sys.exit(-2)
+
+# Load the suds library version information directly into this module without
+# having to import the whole suds library itself. Importing the suds package
+# would have caused problems like the following:
+#   * Forcing the imported package module to be Python 3 compatible without any
+#     lib2to3 fixers first being run on it (since such fixers get run only
+#     later as a part of the setup procedure).
+#   * Making the setup module depend on the package module's dependencies, thus
+#     forcing the user to install them manually (since the setup procedure that
+#     is supposed to install them automatically will not be able to run unless
+#     they are already installed).
+#   We execute explicitly compiled source code instead of having the exec()
+# function compile it to get a better error messages. If we used exec() on the
+# source code directly, the source file would have been listed as just
+# '<string>'.
+exec(read_python_code(os.path.join("suds", "version.py")))
+
+extra_setup_params = {}
+extra_setup_cmdclass = {}
+
+if sys.version_info >= (2, 5):
+    # distutils.setup() 'obsoletes' parameter not introduced until Python 2.5.
+    extra_setup_params["obsoletes"] = ["suds"]
+
+if sys.version_info >= (3, 0):
+    extra_setup_params["use_2to3"] = True
+
+    #   Teach Python's urllib lib2to3 fixer that the old urllib2.__version__
+    # data member is now stored in the urllib.request module.
+    import lib2to3.fixes.fix_urllib
+    for x in lib2to3.fixes.fix_urllib.MAPPING["urllib2"]:
+        if x[0] == "urllib.request":
+            x[1].append("__version__")
+            break;
+
+#   Wrap long_description at 72 characters since PKG-INFO package distribution
+# metadata file stores this text with an 8 space indentation.
+long_description = """
+---------------------------------------
+Lightweight SOAP client (Jurko's fork).
+---------------------------------------
+
+  Based on the original 'suds' project by Jeff Ortel (jortel at redhat
+dot com) hosted at 'http://fedorahosted.org/suds'.
+
+  'Suds' is a lightweight SOAP-based web service client for Python
+licensed under LGPL (see the LICENSE.txt file included in the
+distribution).
+
+  This is hopefully just a temporary fork of the original suds Python
+library project created because the original project development seems
+to have stalled. Should be reintegrated back into the original project
+if it ever gets revived again.
+
+"""
+
+package_name = "suds-jurko"
+version_tag = pkg_resources.safe_version(__version__)
+project_url = "http://bitbucket.org/jurko/suds"
+base_download_url = project_url + "/downloads"
+download_distribution_name = "%s-%s.tar.bz2" % (package_name, version_tag)
+download_url = "%s/%s" % (base_download_url, download_distribution_name)
+
+# Support for integrating running the project' pytest based test suite directly
+# into this setup script so the test suite can be run by 'setup.py test'. Since
+# Python's distutils framework does not allow passing all received command-line
+# arguments to its commands, it does not seem easy to customize how pytest runs
+# its tests this way. To have better control over this, user should run the
+# pytest on the target source tree directly, possibly after first building a
+# temporary one to work around problems like Python 2/3 compatibility.
+import setuptools.command.test
+class PyTest(setuptools.command.test.test):
+    def finalize_options(self):
+        setuptools.command.test.test.finalize_options(self)
+        self.test_args = []
+        self.test_suite = True
+    def run_tests(self):
+        # Make sure the tests are run on the correct test sources. E.g. when
+        # using Python 3, the tests need to be run in the temporary build
+        # folder where they have been previously processed using py2to3.
+        # Running them directly on the original source tree would fail due to
+        # Python 2/3 source code incompatibility.
+        ei_cmd = self.get_finalized_command("egg_info")
+        build_path = setuptools.command.test.normalize_path(ei_cmd.egg_base)
+        test_args = ["--pyargs", build_path]
+        import pytest
+        errno = pytest.main(test_args)
+        sys.exit(errno)
+extra_setup_params.update(tests_require=["pytest"])
+extra_setup_cmdclass.update(test=PyTest)
+
+
 setup(
-    name="suds",
-    version=suds.__version__,
-    description="Lightweight SOAP client",
+    name=package_name,
+    version=__version__,
+    description="Lightweight SOAP client (Jurko's fork)",
+    long_description=long_description,
+    keywords=["SOAP", "web", "service", "client"],
+    url=project_url,
+    download_url=download_url,
+    packages=find_packages(),
+
+    # 'maintainer' will be listed as the distribution package author.
+    # Warning: Due to a 'distribute' package defect when used with Python 3
+    # (verified using 'distribute' package version 0.6.25), given strings must
+    # be given using ASCII characters only. This is needed because 'distribute'
+    # stores the strings by doing a simple write to a PKG-INFO file opened as a
+    # 'default text file' thus attempting to encode the given characters using
+    # the user's default system code-page, e.g. typically CP1250 on eastern
+    # European Windows, CP1252 on western European Windows, UTF-8 on Linux or
+    # any other.
+    #
+    # 'distribute' package merged back with the 'setuptools' package in the
+    # setuptools 0.7 release but we have not yet checked whether this bug has
+    # been corrected there or not.
     author="Jeff Ortel",
     author_email="jortel at redhat.com",
-    maintainer="Jeff Ortel",
-    maintainer_email="jortel at redhat.com",
-    packages=find_packages(exclude=['tests']),
-    url="https://fedorahosted.org/suds",
+    maintainer="Jurko Gospodnetic",
+    maintainer_email="jurko.gospodnetic at pke.hr",
+
+    #   See PEP-301 for the classifier specification. For a complete list of
+    # available classifiers see
+    # 'http://pypi.python.org/pypi?%3Aaction=list_classifiers'.
+    classifiers=["Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: "
+            "GNU Library or Lesser General Public License (LGPL)",
+        "Natural Language :: English",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.4",
+        "Programming Language :: Python :: 2.5",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.0",
+        "Programming Language :: Python :: 3.1",
+        "Programming Language :: Python :: 3.2",
+        "Programming Language :: Python :: 3.3",
+        "Topic :: Internet"],
+
+    #   PEP-314 states that if possible license & platform should be specified
+    # using 'classifiers'.
+    license="(specified using classifiers)",
+    platforms=["(specified using classifiers)"],
+
+    # Register custom distutils commands.
+    cmdclass=extra_setup_cmdclass,
+
+    **extra_setup_params
 )
diff --git a/suds/__init__.py b/suds/__init__.py
index 2ffb50d..1f8a6d9 100644
--- a/suds/__init__.py
+++ b/suds/__init__.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,19 +15,18 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Suds is a lightweight SOAP python client that provides a
-service proxy for Web Services.
+Suds is a lightweight SOAP Python client providing a Web Service proxy.
 """
 
-import os
 import sys
 
+
 #
 # Project properties
 #
 
-__version__ = '0.4.1'
-__build__="(beta) R703-20101015"
+from version import __build__, __version__
+
 
 #
 # Exceptions
@@ -35,49 +34,49 @@ __build__="(beta) R703-20101015"
 
 class MethodNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, "Method not found: '%s'" % name)
-        
+        Exception.__init__(self, u"Method not found: '%s'" % name)
+
 class PortNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, "Port not found: '%s'" % name)
-        
+        Exception.__init__(self, u"Port not found: '%s'" % name)
+
 class ServiceNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, "Service not found: '%s'" % name)
-    
+        Exception.__init__(self, u"Service not found: '%s'" % name)
+
 class TypeNotFound(Exception):
     def __init__(self, name):
-        Exception.__init__(self, "Type not found: '%s'" % tostr(name))
-    
+        Exception.__init__(self, u"Type not found: '%s'" % tostr(name))
+
 class BuildError(Exception):
-    msg = \
-        """
-        An error occured while building a instance of (%s).  As a result
-        the object you requested could not be constructed.  It is recommended
-        that you construct the type manually using a Suds object.
-        Please open a ticket with a description of this error.
+    msg = """
+        An error occurred while building an instance of (%s). As a result the
+        object you requested could not be constructed. It is recommended that
+        you construct the type manually using a Suds object. Please open a
+        ticket with a description of this error.
         Reason: %s
         """
     def __init__(self, name, exception):
         Exception.__init__(self, BuildError.msg % (name, exception))
-        
+
 class SoapHeadersNotPermitted(Exception):
-    msg = \
-        """
-        Method (%s) was invoked with SOAP headers.  The WSDL does not
-        define SOAP headers for this method.  Retry without the soapheaders
-        keyword argument.
+    msg = """
+        Method (%s) was invoked with SOAP headers. The WSDL does not define
+        SOAP headers for this method. Retry without the soapheaders keyword
+        argument.
         """
     def __init__(self, name):
         Exception.__init__(self, self.msg % name)
-    
+
 class WebFault(Exception):
     def __init__(self, fault, document):
         if hasattr(fault, 'faultstring'):
-            Exception.__init__(self, "Server raised fault: '%s'" % fault.faultstring)
+            Exception.__init__(self, u"Server raised fault: '%s'" %
+                fault.faultstring)
         self.fault = fault
         self.document = document
 
+
 #
 # Logging
 #
@@ -86,12 +85,23 @@ class Repr:
     def __init__(self, x):
         self.x = x
     def __str__(self):
-        return repr(self.x)  
+        return repr(self.x)
+
 
 #
 # Utility
 #
 
+class null:
+    """
+    The I{null} object.
+    Used to pass NULL for optional XML nodes.
+    """
+    pass
+
+def objid(obj):
+    return obj.__class__.__name__ + ':' + hex(id(obj))
+
 def tostr(object, encoding=None):
     """ get a unicode safe string representation of an object """
     if isinstance(object, basestring):
@@ -138,17 +148,45 @@ def tostr(object, encoding=None):
         return unicode(object)
     except:
         return str(object)
-    
-class null:
-    """
-    The I{null} object.
-    Used to pass NULL for optional XML nodes.
+
+
+#
+# Python 3 compatibility
+#
+
+if sys.version_info < (3, 0):
+    from cStringIO import StringIO as BytesIO
+else:
+    from io import BytesIO
+
+# Idea from 'http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python'.
+class UnicodeMixin(object):
+    if sys.version_info >= (3, 0):
+        # For Python 3, __str__() and __unicode__() should be identical.
+        __str__ = lambda x: x.__unicode__()
+    else:
+        __str__ = lambda x: unicode(x).encode('utf-8')
+
+#   Used instead of byte literals because they are not supported on Python
+# versions prior to 2.6.
+def byte_str(s='', encoding='utf-8', input_encoding='utf-8', errors='strict'):
     """
-    pass
-    
-def objid(obj):
-    return obj.__class__.__name__\
-        +':'+hex(id(obj))
+    Returns a bytestring version of 's', encoded as specified in 'encoding'.
 
+    Accepts str & unicode objects, interpreting non-unicode strings as byte
+    strings encoded using the given input encoding.
+
+    """
+    assert isinstance(s, basestring)
+    if isinstance(s, unicode):
+        return s.encode(encoding, errors)
+    if s and encoding != input_encoding:
+        return s.decode(input_encoding, errors).encode(encoding, errors)
+    return s
 
-import client
+# Class used to represent a byte string. Useful for asserting that correct
+# string types are being passed around where needed.
+if sys.version_info >= (3, 0):
+    byte_str_class = bytes
+else:
+    byte_str_class = str
diff --git a/suds/argparser.py b/suds/argparser.py
new file mode 100644
index 0000000..84ac026
--- /dev/null
+++ b/suds/argparser.py
@@ -0,0 +1,419 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić jurko.gospodnetic at pke.hr )
+
+"""
+Suds web service operation invocation function argument parser.
+
+See the parse_args() function description for more detailed information.
+
+"""
+
+__all__ = ["parse_args"]
+
+
+def parse_args(method_name, param_defs, args, kwargs, external_param_processor,
+    extra_parameter_errors):
+    """
+    Parse arguments for suds web service operation invocation functions.
+
+    Suds prepares Python function objects for invoking web service operations.
+    This function implements generic binding agnostic part of processing the
+    arguments passed when calling those function objects.
+
+    Argument parsing rules:
+      * Each input parameter element should be represented by single regular
+        Python function argument.
+      * At most one input parameter belonging to a single choice parameter
+        structure may have its value specified as something other than None.
+      * Positional arguments are mapped to choice group input parameters the
+        same as is done for a simple all/sequence group - each in turn.
+
+    Expects to be passed the web service operation's parameter definitions
+    (parameter name, type & optional ancestry information) in order and, based
+    on that, extracts the values for those parameter from the arguments
+    provided in the web service operation invocation call.
+
+    Ancestry information describes parameters constructed based on suds
+    library's automatic input parameter structure unwrapping. It is expected to
+    include the parameter's XSD schema 'ancestry' context, i.e. a list of all
+    the parent XSD schema tags containing the parameter's <element> tag. Such
+    ancestry context provides detailed information about how the parameter's
+    value is expected to be used, especially in relation to other input
+    parameters, e.g. at most one parameter value may be specified for
+    parameters directly belonging to the same choice input group.
+
+    Rules on acceptable ancestry items:
+      * Ancestry item's choice() method must return whether the item
+        represents a <choice> XSD schema tag.
+      * Passed ancestry items are used 'by address' internally and the same XSD
+        schema tag is expected to be identified by the exact same ancestry item
+        object during the whole argument processing.
+
+    During processing, each parameter's definition and value, together with any
+    additional pertinent information collected from the encountered parameter
+    definition structure, is passed on to the provided external parameter
+    processor function. There that information is expected to be used to
+    construct the actual binding specific web service operation invocation
+    request.
+
+    Raises a TypeError exception in case any argument related errors are
+    detected. The exceptions raised have been constructed to make them as
+    similar as possible to their respective exceptions raised during regular
+    Python function argument checking.
+
+    Does not support multiple same-named input parameters.
+
+    """
+    arg_parser = _ArgParser(method_name, param_defs, external_param_processor)
+    return arg_parser(args, kwargs, extra_parameter_errors)
+
+
+class _ArgParser:
+    """Internal argument parser implementation function object."""
+
+    def __init__(self, method_name, param_defs, external_param_processor):
+        self.__method_name = method_name
+        self.__param_defs = param_defs
+        self.__external_param_processor = external_param_processor
+        self.__stack = []
+
+    def __call__(self, args, kwargs, extra_parameter_errors):
+        """
+        Runs the main argument parsing operation.
+
+        Passed args & kwargs objects are not modified during parsing.
+
+        Returns an informative 2-tuple containing the number of required &
+        allowed arguments.
+
+        """
+        assert not self.active(), "recursive argument parsing not allowed"
+        self.__init_run(args, kwargs, extra_parameter_errors)
+        try:
+            self.__process_parameters()
+            return self.__all_parameters_processed()
+        finally:
+            self.__cleanup_run()
+            assert not self.active()
+
+    def active(self):
+        """
+        Return whether this object is currently running argument processing.
+
+        Used to avoid recursively entering argument processing from within an
+        external parameter processor.
+
+        """
+        return bool(self.__stack)
+
+    def __all_parameters_processed(self):
+        """
+        Finish the argument processing.
+
+        Should be called after all the web service operation's parameters have
+        been successfully processed and, afterwards, no further parameter
+        processing is allowed.
+
+        Returns a 2-tuple containing the number of required & allowed
+        arguments.
+
+        See the _ArgParser class description for more detailed information.
+
+        """
+        assert self.active()
+        sentinel_frame = self.__stack[0]
+        self.__pop_frames_above(sentinel_frame)
+        assert len(self.__stack) == 1
+        self.__pop_top_frame()
+        assert not self.active()
+        args_required = sentinel_frame.args_required()
+        args_allowed = sentinel_frame.args_allowed()
+        self.__check_for_extra_arguments(args_required, args_allowed)
+        return args_required, args_allowed
+
+    def __check_for_extra_arguments(self, args_required, args_allowed):
+        """
+        Report an error in case any extra arguments are detected.
+
+        Does nothing if reporting extra arguments as exceptions has not been
+        enabled.
+
+        May only be called after the argument processing has been completed.
+
+        """
+        assert not self.active()
+        if not self.__extra_parameter_errors:
+            return
+
+        if self.__kwargs:
+            param_name = self.__kwargs.keys()[0]
+            if param_name in self.__params_with_arguments:
+                msg = "got multiple values for parameter '%s'"
+            else:
+                msg = "got an unexpected keyword argument '%s'"
+            self.__error(msg % (param_name,))
+
+        if self.__args:
+            def plural_suffix(count):
+                if count == 1:
+                    return ""
+                return "s"
+            def plural_was_were(count):
+                if count == 1:
+                    return "was"
+                return "were"
+            expected = args_required
+            if args_required != args_allowed:
+                expected = "%d to %d" % (args_required, args_allowed)
+            given = self.__args_count
+            msg_parts = ["takes %s positional argument" % (expected,),
+                plural_suffix(expected), " but %d " % (given,),
+                plural_was_were(given), " given"]
+            self.__error("".join(msg_parts))
+
+    def __cleanup_run(self):
+        """Cleans up after a completed argument parsing run."""
+        self.__stack = []
+        assert not self.active()
+
+    def __error(self, message):
+        """Report an argument processing error."""
+        raise TypeError("%s() %s" % (self.__method_name, message))
+
+    def __frame_factory(self, ancestry_item):
+        """Construct a new frame representing the given ancestry item."""
+        frame_class = Frame
+        if ancestry_item is not None and ancestry_item.choice():
+            frame_class = ChoiceFrame
+        return frame_class(ancestry_item, self.__error,
+            self.__extra_parameter_errors)
+
+    def __get_param_value(self, name):
+        """
+        Extract a parameter value from the remaining given arguments.
+
+        Returns a 2-tuple consisting of the following:
+          * Boolean indicating whether an argument has been specified for the
+            requested input parameter.
+          * Parameter value.
+
+        """
+        if self.__args:
+            return True, self.__args.pop(0)
+        try:
+            value = self.__kwargs.pop(name)
+        except KeyError:
+            return False, None
+        return True, value
+
+    def __in_choice_context(self):
+        """
+        Whether we are currently processing a choice parameter group.
+
+        This includes processing a parameter defined directly or indirectly
+        within such a group.
+
+        May only be called during parameter processing or the result will be
+        calculated based on the context left behind by the previous parameter
+        processing if any.
+
+        """
+        for x in self.__stack:
+            if x.__class__ is ChoiceFrame:
+                return True
+        return False
+
+    def __init_run(self, args, kwargs, extra_parameter_errors):
+        """Initializes data for a new argument parsing run."""
+        assert not self.active()
+        self.__args = list(args)
+        self.__kwargs = dict(kwargs)
+        self.__extra_parameter_errors = extra_parameter_errors
+        self.__args_count = len(args) + len(kwargs)
+        self.__params_with_arguments = set()
+        self.__stack = []
+        self.__push_frame(None)
+
+    def __match_ancestry(self, ancestry):
+        """
+        Find frames matching the given ancestry.
+
+        Returns a tuple containing the following:
+          * Topmost frame matching the given ancestry or the bottom-most sentry
+            frame if no frame matches.
+          * Unmatched ancestry part.
+
+        """
+        stack = self.__stack
+        if len(stack) == 1:
+            return stack[0], ancestry
+        previous = stack[0]
+        for frame, n in zip(stack[1:], xrange(len(ancestry))):
+            if frame.id() is not ancestry[n]:
+                return previous, ancestry[n:]
+            previous = frame
+        return frame, ancestry[n + 1:]
+
+    def __pop_frames_above(self, frame):
+        """Pops all the frames above, but not including the given frame."""
+        while self.__stack[-1] is not frame:
+            self.__pop_top_frame()
+        assert self.__stack
+
+    def __pop_top_frame(self):
+        """Pops the top frame off the frame stack."""
+        popped = self.__stack.pop()
+        if self.__stack:
+            self.__stack[-1].process_subframe(popped)
+
+    def __process_parameter(self, param_name, param_type, ancestry=None):
+        """Collect values for a given web service operation input parameter."""
+        assert self.active()
+        param_optional = param_type.optional()
+        has_argument, value = self.__get_param_value(param_name)
+        if has_argument:
+            self.__params_with_arguments.add(param_name)
+        self.__update_context(ancestry)
+        self.__stack[-1].process_parameter(param_optional, value is not None)
+        self.__external_param_processor(param_name, param_type,
+            self.__in_choice_context(), value)
+
+    def __process_parameters(self):
+        """Collect values for given web service operation input parameters."""
+        for pdef in self.__param_defs:
+            self.__process_parameter(*pdef)
+
+    def __push_frame(self, ancestry_item):
+        """Push a new frame on top of the frame stack."""
+        frame = self.__frame_factory(ancestry_item)
+        self.__stack.append(frame)
+
+    def __push_frames(self, ancestry):
+        """
+        Push new frames representing given ancestry items.
+
+        May only be given ancestry items other than None. Ancestry item None
+        represents the internal sentinel item and should never appear in a
+        given parameter's ancestry information.
+
+        """
+        for x in ancestry:
+            assert x is not None
+            self.__push_frame(x)
+
+    def __update_context(self, ancestry):
+        if not ancestry:
+            return
+        match_result = self.__match_ancestry(ancestry)
+        last_matching_frame, unmatched_ancestry = match_result
+        self.__pop_frames_above(last_matching_frame)
+        self.__push_frames(unmatched_ancestry)
+
+
+class Frame:
+    """
+    Base _ArgParser context frame.
+
+    When used directly, as opposed to using a derived class, may represent any
+    input parameter context/ancestry item except a choice order indicator.
+
+    """
+
+    def __init__(self, id, error, extra_parameter_errors):
+        """
+        Construct a new Frame instance.
+
+        Passed error function is used to report any argument checking errors.
+
+        """
+        assert self.__class__ != Frame or not id or not id.choice()
+        self.__id = id
+        self._error = error
+        self._extra_parameter_errors = extra_parameter_errors
+        self._args_allowed = 0
+        self._args_required = 0
+        self._has_value = False
+
+    def args_allowed(self):
+        return self._args_allowed
+
+    def args_required(self):
+        return self._args_required
+
+    def has_value(self):
+        return self._has_value
+
+    def id(self):
+        return self.__id
+
+    def process_parameter(self, optional, has_value):
+        args_required = 1
+        if optional:
+            args_required = 0
+        self._process_item(has_value, 1, args_required)
+
+    def process_subframe(self, subframe):
+        self._process_item(
+            subframe.has_value(),
+            subframe.args_allowed(),
+            subframe.args_required())
+
+    def _process_item(self, has_value, args_allowed, args_required):
+        self._args_allowed += args_allowed
+        self._args_required += args_required
+        if has_value:
+            self._has_value = True
+
+
+class ChoiceFrame(Frame):
+    """
+    _ArgParser context frame representing a choice order indicator.
+
+    A choice requires as many input arguments as are needed to satisfy the
+    least requiring of its items. For example, if we use I(n) to identify an
+    item requiring n parameter, then a choice containing I(2), I(3) & I(7)
+    requires 2 arguments while a choice containing I(5) & I(4) requires 4.
+
+    Accepts an argument for each of its contained elements but allows at most
+    one of its directly contained items to have a defined value.
+
+    """
+
+    def __init__(self, id, error, extra_parameter_errors):
+        assert id.choice()
+        Frame.__init__(self, id, error, extra_parameter_errors)
+        self.__has_item = False
+
+    def _process_item(self, has_value, args_allowed, args_required):
+        self._args_allowed += args_allowed
+        self.__update_args_required_for_item(args_required)
+        self.__update_has_value_for_item(has_value)
+
+    def __update_args_required_for_item(self, item_args_required):
+        if not self.__has_item:
+            self.__has_item = True
+            self._args_required = item_args_required
+            return
+        self._args_required = min(self.args_required(), item_args_required)
+
+    def __update_has_value_for_item(self, item_has_value):
+        if item_has_value:
+            if self.has_value() and self._extra_parameter_errors:
+                self._error("got multiple values for a single choice "
+                    "parameter")
+            self._has_value = True
diff --git a/suds/bindings/__init__.py b/suds/bindings/__init__.py
index 5471eba..1704fa9 100644
--- a/suds/bindings/__init__.py
+++ b/suds/bindings/__init__.py
@@ -1,20 +1,18 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides modules containing classes to support Web Services (SOAP)
-bindings.
-"""
\ No newline at end of file
+Provides modules containing classes to support Web Services (SOAP) bindings.
+"""
diff --git a/suds/bindings/binding.py b/suds/bindings/binding.py
index 4a7a996..62f641c 100644
--- a/suds/bindings/binding.py
+++ b/suds/bindings/binding.py
@@ -1,87 +1,76 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Provides classes for (WS) SOAP bindings.
 """
 
-from logging import getLogger
 from suds import *
 from suds.sax import Namespace
-from suds.sax.parser import Parser
 from suds.sax.document import Document
 from suds.sax.element import Element
-from suds.sudsobject import Factory, Object
+from suds.sudsobject import Factory
 from suds.mx import Content
 from suds.mx.literal import Literal as MxLiteral
-from suds.umx.basic import Basic as UmxBasic
 from suds.umx.typed import Typed as UmxTyped
 from suds.bindings.multiref import MultiRef
 from suds.xsd.query import TypeQuery, ElementQuery
 from suds.xsd.sxbasic import Element as SchemaElement
 from suds.options import Options
 from suds.plugin import PluginContainer
-from copy import deepcopy 
 
-log = getLogger(__name__)
+from copy import deepcopy
+
 
 envns = ('SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/')
 
 
 class Binding:
     """
-    The soap binding class used to process outgoing and imcoming
-    soap messages per the WSDL port binding.
-    @cvar replyfilter: The reply filter function.
-    @type replyfilter: (lambda s,r: r)
-    @ivar wsdl: The wsdl.
+    The SOAP binding class used to process outgoing and incoming SOAP messages
+    per the WSDL port binding.
+    @ivar wsdl: The WSDL.
     @type wsdl: L{suds.wsdl.Definitions}
-    @ivar schema: The collective schema contained within the wsdl.
+    @ivar schema: The collective schema contained within the WSDL.
     @type schema: L{xsd.schema.Schema}
     @ivar options: A dictionary options.
     @type options: L{Options}
     """
-    
-    replyfilter = (lambda s,r: r)
 
     def __init__(self, wsdl):
         """
-        @param wsdl: A wsdl.
+        @param wsdl: A WSDL.
         @type wsdl: L{wsdl.Definitions}
         """
         self.wsdl = wsdl
         self.multiref = MultiRef()
-        
+
     def schema(self):
         return self.wsdl.schema
-    
+
     def options(self):
         return self.wsdl.options
-        
-    def unmarshaller(self, typed=True):
+
+    def unmarshaller(self):
         """
-        Get the appropriate XML decoder.
-        @return: Either the (basic|typed) unmarshaller.
+        Get the appropriate schema based XML decoder.
+        @return: Typed unmarshaller.
         @rtype: L{UmxTyped}
         """
-        if typed:
-            return UmxTyped(self.schema())
-        else:
-            return UmxBasic()
-        
+        return UmxTyped(self.schema())
+
     def marshaller(self):
         """
         Get the appropriate XML encoder.
@@ -89,12 +78,12 @@ class Binding:
         @rtype: L{MxLiteral}
         """
         return MxLiteral(self.schema(), self.options().xstq)
-    
+
     def param_defs(self, method):
         """
-        Get parameter definitions.  
+        Get parameter definitions.
         Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject})
-        @param method: A servic emethod.
+        @param method: A service method.
         @type method: I{service.Method}
         @return: A collection of parameter definitions
         @rtype: [I{pdef},..]
@@ -103,15 +92,15 @@ class Binding:
 
     def get_message(self, method, args, kwargs):
         """
-        Get the soap message for the specified method, args and soapheaders.
-        This is the entry point for creating the outbound soap message.
+        Get a SOAP message for the specified method, args and SOAP headers.
+        This is the entry point for creating an outbound SOAP message.
         @param method: The method being invoked.
         @type method: I{service.Method}
         @param args: A list of args for the method invoked.
         @type args: list
         @param kwargs: Named (keyword) args for the method invoked.
         @type kwargs: dict
-        @return: The soap envelope.
+        @return: The SOAP envelope.
         @rtype: L{Document}
         """
 
@@ -126,67 +115,41 @@ class Binding:
         else:
             env.refitPrefixes()
         return Document(env)
-    
-    def get_reply(self, method, reply):
+
+    def get_reply(self, method, replyroot):
         """
-        Process the I{reply} for the specified I{method} by sax parsing the I{reply}
-        and then unmarshalling into python object(s).
+        Process the I{reply} for the specified I{method} by unmarshalling it
+        into into Python object(s).
         @param method: The name of the invoked method.
         @type method: str
-        @param reply: The reply XML received after invoking the specified method.
-        @type reply: str
-        @return: The unmarshalled reply.  The returned value is an L{Object} for a
-            I{list} depending on whether the service returns a single object or a 
-            collection.
-        @rtype: tuple ( L{Element}, L{Object} )
-        """
-        reply = self.replyfilter(reply)
-        sax = Parser()
-        replyroot = sax.parse(string=reply)
-        plugins = PluginContainer(self.options().plugins)
-        plugins.message.parsed(reply=replyroot)
-        soapenv = replyroot.getChild('Envelope')
+        @param replyroot: The reply XML root node received after invoking the
+            specified method.
+        @type reply: L{Element}
+        @return: The unmarshalled reply.  The returned value is an L{Object} or
+            a I{list} depending on whether the service returns a single object
+            or a collection.
+        @rtype: L{Object} or I{list}
+        """
+        soapenv = replyroot.getChild('Envelope', envns)
         soapenv.promotePrefixes()
-        soapbody = soapenv.getChild('Body')
-        self.detect_fault(soapbody)
+        soapbody = soapenv.getChild('Body', envns)
         soapbody = self.multiref.process(soapbody)
         nodes = self.replycontent(method, soapbody)
         rtypes = self.returned_types(method)
         if len(rtypes) > 1:
-            result = self.replycomposite(rtypes, nodes)
-            return (replyroot, result)
-        if len(rtypes) == 1:
-            if rtypes[0].unbounded():
-                result = self.replylist(rtypes[0], nodes)
-                return (replyroot, result)
-            if len(nodes):
-                unmarshaller = self.unmarshaller()
-                resolved = rtypes[0].resolve(nobuiltin=True)
-                result = unmarshaller.process(nodes[0], resolved)
-                return (replyroot, result)
-        return (replyroot, None)
-    
-    def detect_fault(self, body):
-        """
-        Detect I{hidden} soapenv:Fault element in the soap body.
-        @param body: The soap envelope body.
-        @type body: L{Element}
-        @raise WebFault: When found.
-        """
-        fault = body.getChild('Fault', envns)
-        if fault is None:
+            return self.replycomposite(rtypes, nodes)
+        if len(rtypes) == 0:
             return
-        unmarshaller = self.unmarshaller(False)
-        p = unmarshaller.process(fault)
-        if self.options().faults:
-            raise WebFault(p, fault)
-        return self
-        
-    
+        if rtypes[0].multi_occurrence():
+            return self.replylist(rtypes[0], nodes)
+        if len(nodes):
+            resolved = rtypes[0].resolve(nobuiltin=True)
+            return self.unmarshaller().process(nodes[0], resolved)
+
     def replylist(self, rt, nodes):
         """
-        Construct a I{list} reply.  This mehod is called when it has been detected
-        that the reply is a list.
+        Construct a I{list} reply. This mehod is called when it has been
+        detected that the reply is a list.
         @param rt: The return I{type}.
         @type rt: L{suds.xsd.sxbase.SchemaObject}
         @param nodes: A collection of XML nodes.
@@ -201,10 +164,10 @@ class Binding:
             sobject = unmarshaller.process(node, resolved)
             result.append(sobject)
         return result
-    
+
     def replycomposite(self, rtypes, nodes):
         """
-        Construct a I{composite} reply.  This method is called when it has been
+        Construct a I{composite} reply. This method is called when it has been
         detected that the reply has multiple root nodes.
         @param rtypes: A list of known return I{types}.
         @type rtypes: [L{suds.xsd.sxbase.SchemaObject},...]
@@ -230,7 +193,7 @@ class Binding:
             sobject = unmarshaller.process(node, resolved)
             value = getattr(composite, tag, None)
             if value is None:
-                if rt.unbounded():
+                if rt.multi_occurrence():
                     value = []
                     setattr(composite, tag, value)
                     value.append(sobject)
@@ -240,31 +203,9 @@ class Binding:
                 if not isinstance(value, list):
                     value = [value,]
                     setattr(composite, tag, value)
-                value.append(sobject)          
+                value.append(sobject)
         return composite
-    
-    def get_fault(self, reply):
-        """
-        Extract the fault from the specified soap reply.  If I{faults} is True, an
-        exception is raised.  Otherwise, the I{unmarshalled} fault L{Object} is
-        returned.  This method is called when the server raises a I{web fault}.
-        @param reply: A soap reply message.
-        @type reply: str
-        @return: A fault object.
-        @rtype: tuple ( L{Element}, L{Object} )
-        """
-        reply = self.replyfilter(reply)
-        sax = Parser()
-        faultroot = sax.parse(string=reply)
-        soapenv = faultroot.getChild('Envelope')
-        soapbody = soapenv.getChild('Body')
-        fault = soapbody.getChild('Fault')
-        unmarshaller = self.unmarshaller(False)
-        p = unmarshaller.process(fault)
-        if self.options().faults:
-            raise WebFault(p, faultroot)
-        return (faultroot, p.detail)
-    
+
     def mkparam(self, method, pdef, object):
         """
         Builds a parameter for the specified I{method} using the parameter
@@ -279,13 +220,10 @@ class Binding:
         @rtype: L{Element}
         """
         marshaller = self.marshaller()
-        content = \
-            Content(tag=pdef[0],
-                    value=object, 
-                    type=pdef[1], 
-                    real=pdef[1].resolve())
+        content = Content(tag=pdef[0], value=object, type=pdef[1],
+            real=pdef[1].resolve())
         return marshaller.process(content)
-    
+
     def mkheader(self, method, hdef, object):
         """
         Builds a soapheader for the specified I{method} using the header
@@ -307,15 +245,15 @@ class Binding:
             return tags
         content = Content(tag=hdef[0], value=object, type=hdef[1])
         return marshaller.process(content)
-            
+
     def envelope(self, header, body):
         """
-        Build the B{<Envelope/>} for an soap outbound message.
-        @param header: The soap message B{header}.
+        Build the B{<Envelope/>} for a SOAP outbound message.
+        @param header: The SOAP message B{header}.
         @type header: L{Element}
-        @param body: The soap message B{body}.
+        @param body: The SOAP message B{body}.
         @type body: L{Element}
-        @return: The soap envelope containing the body and header.
+        @return: The SOAP envelope containing the body and header.
         @rtype: L{Element}
         """
         env = Element('Envelope', ns=envns)
@@ -323,39 +261,39 @@ class Binding:
         env.append(header)
         env.append(body)
         return env
-    
+
     def header(self, content):
         """
-        Build the B{<Body/>} for an soap outbound message.
+        Build the B{<Body/>} for a SOAP outbound message.
         @param content: The header content.
         @type content: L{Element}
-        @return: the soap body fragment.
+        @return: the SOAP body fragment.
         @rtype: L{Element}
         """
         header = Element('Header', ns=envns)
         header.append(content)
         return header
-    
+
     def bodycontent(self, method, args, kwargs):
         """
-        Get the content for the soap I{body} node.
+        Get the content for the SOAP I{body} node.
         @param method: A service method.
         @type method: I{service.Method}
         @param args: method parameter values
         @type args: list
         @param kwargs: Named (keyword) args for the method invoked.
         @type kwargs: dict
-        @return: The xml content for the <body/>
+        @return: The XML content for the <body/>
         @rtype: [L{Element},..]
         """
         raise Exception, 'not implemented'
-    
+
     def headercontent(self, method):
         """
-        Get the content for the soap I{Header} node.
+        Get the content for the SOAP I{Header} node.
         @param method: A service method.
         @type method: I{service.Method}
-        @return: The xml content for the <body/>
+        @return: The XML content for the <body/>
         @rtype: [L{Element},..]
         """
         n = 0
@@ -390,35 +328,35 @@ class Binding:
                 h.setPrefix(ns[0], ns[1])
                 content.append(h)
         return content
-    
+
     def replycontent(self, method, body):
         """
         Get the reply body content.
         @param method: A service method.
         @type method: I{service.Method}
-        @param body: The soap body
+        @param body: The SOAP body.
         @type body: L{Element}
-        @return: the body content
+        @return: The body content.
         @rtype: [L{Element},...]
         """
         raise Exception, 'not implemented'
-    
+
     def body(self, content):
         """
-        Build the B{<Body/>} for an soap outbound message.
+        Build the B{<Body/>} for a SOAP outbound message.
         @param content: The body content.
         @type content: L{Element}
-        @return: the soap body fragment.
+        @return: The SOAP body fragment.
         @rtype: L{Element}
         """
         body = Element('Body', ns=envns)
         body.append(content)
         return body
-    
+
     def bodypart_types(self, method, input=True):
         """
-        Get a list of I{parameter definitions} (pdef) defined for the specified method.
-        Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject})
+        Get a list of I{parameter definitions} (pdef) defined for the specified
+        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
         @param method: A service method.
         @type method: I{service.Method}
         @param input: Defines input/output message.
@@ -449,11 +387,11 @@ class Binding:
             else:
                 result.append(pt)
         return result
-    
+
     def headpart_types(self, method, input=True):
         """
-        Get a list of I{parameter definitions} (pdef) defined for the specified method.
-        Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject})
+        Get a list of I{parameter definitions} (pdef) defined for the specified
+        method. Each I{pdef} is a tuple (I{name}, L{xsd.sxbase.SchemaObject}).
         @param method: A service method.
         @type method: I{service.Method}
         @param input: Defines input/output message.
@@ -485,7 +423,7 @@ class Binding:
             else:
                 result.append(pt)
         return result
-    
+
     def returned_types(self, method):
         """
         Get the L{xsd.sxbase.SchemaObject} returned by the I{method}.
@@ -507,7 +445,7 @@ class PartElement(SchemaElement):
     @ivar resolved: The part type.
     @type resolved: L{suds.xsd.sxbase.SchemaObject}
     """
-    
+
     def __init__(self, name, resolved):
         """
         @param name: The part name.
@@ -520,19 +458,17 @@ class PartElement(SchemaElement):
         self.__resolved = resolved
         self.name = name
         self.form_qualified = False
-        
+
     def implany(self):
         return self
-    
+
     def optional(self):
         return True
-        
+
     def namespace(self, prefix=None):
         return Namespace.default
-        
+
     def resolve(self, nobuiltin=False):
         if nobuiltin and self.__resolved.builtin():
             return self
-        else:
-            return self.__resolved
-    
\ No newline at end of file
+        return self.__resolved
diff --git a/suds/bindings/document.py b/suds/bindings/document.py
index cace0d5..84ef566 100644
--- a/suds/bindings/document.py
+++ b/suds/bindings/document.py
@@ -1,50 +1,59 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Provides classes for the (WS) SOAP I{document/literal}.
+Provides classes for the (WS) SOAP I{document/literal} binding.
+
 """
 
-from logging import getLogger
 from suds import *
+from suds.argparser import parse_args
 from suds.bindings.binding import Binding
 from suds.sax.element import Element
 
-log = getLogger(__name__)
-
 
 class Document(Binding):
     """
-    The document/literal style.  Literal is the only (@use) supported
-    since document/encoded is pretty much dead.
-    Although the soap specification supports multiple documents within the soap
-    <body/>, it is very uncommon.  As such, suds presents an I{RPC} view of
-    service methods defined with a single document parameter.  This is done so 
-    that the user can pass individual parameters instead of one, single document.
-    To support the complete specification, service methods defined with multiple documents
-    (multiple message parts), must present a I{document} view for that method.
+    The document/literal style. Literal is the only (@use) supported since
+    document/encoded is pretty much dead.
+
+    Although the SOAP specification supports multiple documents within the SOAP
+    <body/>, it is very uncommon. As such, suds library supports presenting an
+    I{RPC} view of service methods defined with only a single document
+    parameter. To support the complete specification, service methods defined
+    with multiple documents (multiple message parts), are still presented using
+    a full I{document} view.
+
+    More detailed description:
+
+    An interface is considered I{wrapped} if:
+      - There is exactly one message part in that interface.
+      - The message part resolves to an element of a non-builtin type.
+    Otherwise it is considered I{bare}.
+
+    I{Bare} interface is interpreted directly as specified in the WSDL schema,
+    with each message part represented by a single parameter in the suds
+    library web service operation proxy interface (input or output).
+
+    I{Wrapped} interface is interpreted without the external wrapping document
+    structure, with each of its contained elements passed through suds
+    library's web service operation proxy interface (input or output)
+    individually instead of as a single I{document} object.
+
     """
-        
     def bodycontent(self, method, args, kwargs):
-        #
-        # The I{wrapped} vs I{bare} style is detected in 2 ways.
-        # If there is 2+ parts in the message then it is I{bare}.
-        # If there is only (1) part and that part resolves to a builtin then
-        # it is I{bare}.  Otherwise, it is I{wrapped}.
-        #
         if not len(method.soap.input.body.parts):
             return ()
         wrapped = method.soap.input.body.wrapped
@@ -53,85 +62,86 @@ class Document(Binding):
             root = self.document(pts[0])
         else:
             root = []
-        n = 0
-        for pd in self.param_defs(method):
-            if n < len(args):
-                value = args[n]
-            else:
-                value = kwargs.get(pd[0])
-            n += 1
-            p = self.mkparam(method, pd, value)
+
+        def add_param(param_name, param_type, in_choice_context, value):
+            """
+            Construct request data for the given input parameter.
+
+            Called by our argument parser for every input parameter, in order.
+
+            """
+            # Do not construct request data for undefined input parameters
+            # defined inside a choice order indicator. An empty choice
+            # parameter can still be included in the constructed request by
+            # explicitly providing an empty string value for it.
+            #TODO: This functionality might be better placed inside the
+            # mkparam() function but to do that we would first need to better
+            # understand how different Binding subclasses in suds work and how
+            # they would be affected by this change.
+            if in_choice_context and value is None:
+                return
+
+            # Construct request data for the current input parameter.
+            pdef = (param_name, param_type)
+            p = self.mkparam(method, pdef, value)
             if p is None:
-                continue
+                return
             if not wrapped:
-                ns = pd[1].namespace('ns0')
+                ns = param_type.namespace("ns0")
                 p.setPrefix(ns[0], ns[1])
             root.append(p)
+
+        parse_args(method.name, self.param_defs(method), args, kwargs,
+            add_param, self.options().extraArgumentErrors)
+
         return root
 
     def replycontent(self, method, body):
         wrapped = method.soap.output.body.wrapped
         if wrapped:
             return body[0].children
-        else:
-            return body.children
-        
+        return body.children
+
     def document(self, wrapper):
         """
-        Get the document root.  For I{document/literal}, this is the
-        name of the wrapper element qualifed by the schema tns.
+        Get the document root. For I{document/literal}, this is the name of the
+        wrapper element qualified by the schema's target namespace.
         @param wrapper: The method name.
         @type wrapper: L{xsd.sxbase.SchemaObject}
         @return: A root element.
         @rtype: L{Element}
         """
         tag = wrapper[1].name
-        ns = wrapper[1].namespace('ns0')
-        d = Element(tag, ns=ns)
-        return d
-    
+        ns = wrapper[1].namespace("ns0")
+        return Element(tag, ns=ns)
+
     def mkparam(self, method, pdef, object):
-        #
-        # Expand list parameters into individual parameters
-        # each with the type information.  This is because in document
-        # arrays are simply unbounded elements.
-        #
+        """
+        Expand list parameters into individual parameters each with the type
+        information. This is because in document arrays are simply
+        multi-occurrence elements.
+
+        """
         if isinstance(object, (list, tuple)):
             tags = []
             for item in object:
                 tags.append(self.mkparam(method, pdef, item))
             return tags
-        else:
-            return Binding.mkparam(self, method, pdef, object)
-        
+        return Binding.mkparam(self, method, pdef, object)
+
     def param_defs(self, method):
-        #
-        # Get parameter definitions for document literal.
-        # The I{wrapped} vs I{bare} style is detected in 2 ways.
-        # If there is 2+ parts in the message then it is I{bare}.
-        # If there is only (1) part and that part resolves to a builtin then
-        # it is I{bare}.  Otherwise, it is I{wrapped}.
-        #
+        """Get parameter definitions for document literal."""
         pts = self.bodypart_types(method)
         wrapped = method.soap.input.body.wrapped
         if not wrapped:
             return pts
         result = []
-        # wrapped
         for p in pts:
-            resolved = p[1].resolve()
-            for child, ancestry in resolved:
-                if child.isattr():
-                    continue
-                if self.bychoice(ancestry):
-                    log.debug(
-                        '%s\ncontained by <choice/>, excluded as param for %s()',
-                        child,
-                        method.name)
-                    continue
-                result.append((child.name, child))
+            for child, ancestry in p[1].resolve():
+                if not child.isattr():
+                    result.append((child.name, child, ancestry))
         return result
-    
+
     def returned_types(self, method):
         result = []
         wrapped = method.soap.output.body.wrapped
@@ -145,16 +155,3 @@ class Document(Binding):
         else:
             result += rts
         return result
-    
-    def bychoice(self, ancestry):
-        """
-        The ancestry contains a <choice/>
-        @param ancestry: A list of ancestors.
-        @type ancestry: list
-        @return: True if contains <choice/>
-        @rtype: boolean
-        """
-        for x in ancestry:
-            if x.choice():
-                return True
-        return False
\ No newline at end of file
diff --git a/suds/bindings/multiref.py b/suds/bindings/multiref.py
index e539592..52fa47a 100644
--- a/suds/bindings/multiref.py
+++ b/suds/bindings/multiref.py
@@ -1,28 +1,25 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Provides classes for handling soap multirefs.
 """
 
-from logging import getLogger
 from suds import *
 from suds.sax.element import Element
 
-log = getLogger(__name__)
 
 soapenc = (None, 'http://schemas.xmlsoap.org/soap/encoding/')
 
@@ -32,13 +29,13 @@ class MultiRef:
     @ivar nodes: A list of non-multiref nodes.
     @type nodes: list
     @ivar catalog: A dictionary of multiref nodes by id.
-    @type catalog: dict 
+    @type catalog: dict
     """
-    
+
     def __init__(self):
         self.nodes = []
         self.catalog = {}
-    
+
     def process(self, body):
         """
         Process the specified soap envelope body and replace I{multiref} node
@@ -54,7 +51,7 @@ class MultiRef:
         self.update(body)
         body.children = self.nodes
         return body
-    
+
     def update(self, node):
         """
         Update the specified I{node} by replacing the I{multiref} references with
@@ -68,12 +65,12 @@ class MultiRef:
         for c in node.children:
             self.update(c)
         return node
-            
+
     def replace_references(self, node):
         """
-        Replacing the I{multiref} references with the contents of the 
+        Replacing the I{multiref} references with the contents of the
         referenced nodes and remove the I{href} attribute.  Warning:  since
-        the I{ref} is not cloned, 
+        the I{ref} is not cloned,
         @param node: A node to update.
         @type node: L{Element}
         """
@@ -83,6 +80,8 @@ class MultiRef:
         id = href.getValue()
         ref = self.catalog.get(id)
         if ref is None:
+            import logging
+            log = logging.getLogger(__name__)
             log.error('soap multiref: %s, not-resolved', id)
             return
         node.append(ref.children)
@@ -91,7 +90,7 @@ class MultiRef:
             if a.name != 'id':
                 node.append(a)
         node.remove(href)
-            
+
     def build_catalog(self, body):
         """
         Create the I{catalog} of multiref nodes by id and the list of
@@ -123,4 +122,3 @@ class MultiRef:
             return True
         else:
             return ( root.value == '1' )
-        
\ No newline at end of file
diff --git a/suds/bindings/rpc.py b/suds/bindings/rpc.py
index f780aa4..d164960 100644
--- a/suds/bindings/rpc.py
+++ b/suds/bindings/rpc.py
@@ -1,32 +1,28 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Provides classes for the (WS) SOAP I{rpc/literal} and I{rpc/encoded} bindings.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx.encoded import Encoded as MxEncoded
 from suds.umx.encoded import Encoded as UmxEncoded
 from suds.bindings.binding import Binding, envns
 from suds.sax.element import Element
 
-log = getLogger(__name__)
-
 
 encns = ('SOAP-ENC', 'http://schemas.xmlsoap.org/soap/encoding/')
 
@@ -34,17 +30,17 @@ class RPC(Binding):
     """
     RPC/Literal binding style.
     """
-    
+
     def param_defs(self, method):
         return self.bodypart_types(method)
-        
+
     def envelope(self, header, body):
         env = Binding.envelope(self, header, body)
         env.addPrefix(encns[0], encns[1])
-        env.set('%s:encodingStyle' % envns[0], 
+        env.set('%s:encodingStyle' % envns[0],
                 'http://schemas.xmlsoap.org/soap/encoding/')
         return env
-        
+
     def bodycontent(self, method, args, kwargs):
         n = 0
         root = self.method(method)
@@ -58,14 +54,14 @@ class RPC(Binding):
                 root.append(p)
             n += 1
         return root
-    
+
     def replycontent(self, method, body):
         return body[0].children
-        
+
     def method(self, method):
         """
         Get the document root.  For I{rpc/(literal|encoded)}, this is the
-        name of the method qualifed by the schema tns.
+        name of the method qualified by the schema tns.
         @param method: A service method.
         @type method: I{service.Method}
         @return: A root element.
@@ -76,7 +72,7 @@ class RPC(Binding):
             ns = ('ns0', ns[1])
         method = Element(method.name, ns=ns)
         return method
-    
+
 
 class Encoded(RPC):
     """
@@ -86,13 +82,10 @@ class Encoded(RPC):
     def marshaller(self):
         return MxEncoded(self.schema())
 
-    def unmarshaller(self, typed=True):
+    def unmarshaller(self):
         """
-        Get the appropriate XML decoder.
-        @return: Either the (basic|typed) unmarshaller.
+        Get the appropriate schema based XML decoder.
+        @return: Typed unmarshaller.
         @rtype: L{UmxTyped}
         """
-        if typed:
-            return UmxEncoded(self.schema())
-        else:
-            return RPC.unmarshaller(self, typed)
+        return UmxEncoded(self.schema())
diff --git a/suds/builder.py b/suds/builder.py
index c2aad98..8ebfb38 100644
--- a/suds/builder.py
+++ b/suds/builder.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,23 +18,20 @@
 The I{builder} module provides an wsdl/xsd defined types factory
 """
 
-from logging import getLogger
 from suds import *
 from suds.sudsobject import Factory
 
-log = getLogger(__name__)
-
 
 class Builder:
     """ Builder used to construct an object for types defined in the schema """
-    
+
     def __init__(self, resolver):
         """
         @param resolver: A schema object name resolver.
         @type resolver: L{resolver.Resolver}
         """
         self.resolver = resolver
-        
+
     def build(self, name):
         """ build a an object for the specified typename as defined in the schema """
         if isinstance(name, basestring):
@@ -59,7 +56,7 @@ class Builder:
                 continue
             self.process(data, child, history[:])
         return data
-            
+
     def process(self, data, type, history):
         """ process the specified type then process its children """
         if type in history:
@@ -69,7 +66,7 @@ class Builder:
         history.append(type)
         resolved = type.resolve()
         value = None
-        if type.unbounded():
+        if type.multi_occurrence():
             value = []
         else:
             if len(resolved) > 0:
@@ -98,7 +95,7 @@ class Builder:
             name = '_%s' % attr.name
             value = attr.get_default()
             setattr(data, name, value)
-                
+
     def skip_child(self, child, ancestry):
         """ get whether or not to skip the specified child """
         if child.any(): return True
@@ -106,7 +103,7 @@ class Builder:
             if x.choice():
                 return True
         return False
-    
+
     def ordering(self, type):
         """ get the ordering """
         result = []
@@ -118,4 +115,3 @@ class Builder:
                 name = '_%s' % child.name
             result.append(name)
         return result
-            
\ No newline at end of file
diff --git a/suds/cache.py b/suds/cache.py
index 801c23c..e156d19 100644
--- a/suds/cache.py
+++ b/suds/cache.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,21 +18,21 @@
 Contains basic caching classes.
 """
 
-import os
 import suds
-from tempfile import gettempdir as tmp
 from suds.transport import *
 from suds.sax.parser import Parser
 from suds.sax.element import Element
+
 from datetime import datetime as dt
 from datetime import timedelta
-from cStringIO import StringIO
-from logging import getLogger
+import os
+from tempfile import gettempdir as tmp
 try:
     import cPickle as pickle
-except:
+except Exception:
     import pickle
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -50,17 +50,7 @@ class Cache:
         @rtype: any
         """
         raise Exception('not-implemented')
-    
-    def getf(self, id):
-        """
-        Get a object from the cache by ID.
-        @param id: The object ID.
-        @type id: str
-        @return: The object, else None
-        @rtype: any
-        """
-        raise Exception('not-implemented')
-    
+
     def put(self, id, object):
         """
         Put a object into the cache.
@@ -70,47 +60,31 @@ class Cache:
         @type object: any
         """
         raise Exception('not-implemented')
-    
-    def putf(self, id, fp):
-        """
-        Write a fp into the cache.
-        @param id: The object ID.
-        @type id: str
-        @param fp: File pointer.
-        @type fp: file-like object.
-        """
-        raise Exception('not-implemented')
-    
+
     def purge(self, id):
         """
         Purge a object from the cache by id.
         @param id: A object ID.
-        @type id: str        
+        @type id: str
         """
         raise Exception('not-implemented')
-    
+
     def clear(self):
         """
         Clear all objects from the cache.
         """
         raise Exception('not-implemented')
-    
+
 
 class NoCache(Cache):
     """
     The passthru object cache.
     """
-    
+
     def get(self, id):
         return None
-    
-    def getf(self, id):
-        return None
-    
-    def put(self, id, object):
-        pass
 
-    def putf(self, id, fp):
+    def put(self, id, object):
         pass
 
 
@@ -127,7 +101,7 @@ class FileCache(Cache):
     """
     fnprefix = 'suds'
     units = ('months', 'weeks', 'days', 'hours', 'minutes', 'seconds')
-    
+
     def __init__(self, location=None, **duration):
         """
         @param location: The directory for the cached files.
@@ -143,7 +117,7 @@ class FileCache(Cache):
         self.duration = (None, 0)
         self.setduration(**duration)
         self.checkversion()
-        
+
     def fnsuffix(self):
         """
         Get the file name suffix
@@ -151,10 +125,10 @@ class FileCache(Cache):
         @rtype: str
         """
         return 'gcf'
-        
+
     def setduration(self, **duration):
         """
-        Set the caching duration which defines how long the 
+        Set the caching duration which defines how long the
         file will be cached.
         @param duration: The cached file duration which defines how
             long the file will be cached.  A duration=0 means forever.
@@ -167,7 +141,7 @@ class FileCache(Cache):
                 raise Exception('must be: %s' % str(self.units))
             self.duration = arg
         return self
-    
+
     def setlocation(self, location):
         """
         Set the location (directory) for the cached files.
@@ -175,7 +149,7 @@ class FileCache(Cache):
         @type location: str
         """
         self.location = location
-            
+
     def mktmp(self):
         """
         Make the I{location} directory if it doesn't already exits.
@@ -183,48 +157,39 @@ class FileCache(Cache):
         try:
             if not os.path.isdir(self.location):
                 os.makedirs(self.location)
-        except:
+        except Exception:
             log.debug(self.location, exc_info=1)
         return self
-    
+
     def put(self, id, bfr):
         try:
             fn = self.__fn(id)
-            f = self.open(fn, 'w')
-            f.write(bfr)
-            f.close()
+            f = self.open(fn, 'wb')
+            try:
+                f.write(bfr)
+            finally:
+                f.close()
             return bfr
-        except:
+        except Exception:
             log.debug(id, exc_info=1)
             return bfr
-        
-    def putf(self, id, fp):
-        try:
-            fn = self.__fn(id)
-            f = self.open(fn, 'w')
-            f.write(fp.read())
-            fp.close()
-            f.close()
-            return open(fn)
-        except:
-            log.debug(id, exc_info=1)
-            return fp
-        
+
     def get(self, id):
         try:
             f = self.getf(id)
-            bfr = f.read()
-            f.close()
-            return bfr
-        except:
+            try:
+                return f.read()
+            finally:
+                f.close()
+        except Exception:
             pass
-    
+
     def getf(self, id):
         try:
             fn = self.__fn(id)
             self.validate(fn)
-            return self.open(fn)
-        except:
+            return self.open(fn, 'rb')
+        except Exception:
             pass
 
     def validate(self, fn):
@@ -236,77 +201,77 @@ class FileCache(Cache):
         if self.duration[1] < 1:
             return
         created = dt.fromtimestamp(os.path.getctime(fn))
-        d = { self.duration[0]:self.duration[1] }
-        expired = created+timedelta(**d)
+        d = {self.duration[0]:self.duration[1]}
+        expired = created + timedelta(**d)
         if expired < dt.now():
             log.debug('%s expired, deleted', fn)
             os.remove(fn)
- 
+
     def clear(self):
         for fn in os.listdir(self.location):
-            if os.path.isdir(fn):
+            path = os.path.join(self.location, fn)
+            if os.path.isdir(path):
                 continue
             if fn.startswith(self.fnprefix):
-                log.debug('deleted: %s', fn)
-                os.remove(os.path.join(self.location, fn))
-                
+                os.remove(path)
+                log.debug('deleted: %s', path)
+
     def purge(self, id):
         fn = self.__fn(id)
         try:
             os.remove(fn)
-        except:
+        except Exception:
             pass
-                
+
     def open(self, fn, *args):
         """
         Open the cache file making sure the directory is created.
         """
         self.mktmp()
         return open(fn, *args)
-    
+
     def checkversion(self):
         path = os.path.join(self.location, 'version')
         try:
-            
             f = self.open(path)
             version = f.read()
             f.close()
             if version != suds.__version__:
                 raise Exception()
-        except:
+        except Exception:
             self.clear()
             f = self.open(path, 'w')
             f.write(suds.__version__)
-            f.close()        
-    
+            f.close()
+
     def __fn(self, id):
         name = id
         suffix = self.fnsuffix()
         fn = '%s-%s.%s' % (self.fnprefix, name, suffix)
         return os.path.join(self.location, fn)
-    
-    
+
+
 class DocumentCache(FileCache):
     """
     Provides xml document caching.
     """
-    
+
     def fnsuffix(self):
         return 'xml'
-    
+
     def get(self, id):
         try:
-            fp = FileCache.getf(self, id)
+            fp = self.getf(id)
             if fp is None:
                 return None
             p = Parser()
             return p.parse(fp)
-        except:
-            FileCache.purge(self, id)
-    
+        except Exception:
+            self.purge(id)
+
     def put(self, id, object):
         if isinstance(object, Element):
-            FileCache.put(self, id, str(object))
+            FileCache.put(self, id, suds.byte_str(str(object)))
         return object
 
 
@@ -317,20 +282,19 @@ class ObjectCache(FileCache):
     @type protocol: int
     """
     protocol = 2
-    
+
     def fnsuffix(self):
         return 'px'
-    
+
     def get(self, id):
         try:
-            fp = FileCache.getf(self, id)
+            fp = self.getf(id)
             if fp is None:
                 return None
-            else:
-                return pickle.load(fp)
-        except:
-            FileCache.purge(self, id)
-    
+            return pickle.load(fp)
+        except Exception:
+            self.purge(id)
+
     def put(self, id, object):
         bfr = pickle.dumps(object, self.protocol)
         FileCache.put(self, id, bfr)
diff --git a/suds/client.py b/suds/client.py
index 1692b26..d7fda60 100644
--- a/suds/client.py
+++ b/suds/client.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -20,34 +20,36 @@ See I{README.txt}
 """
 
 import suds
-import suds.metrics as metrics
-from cookielib import CookieJar
 from suds import *
-from suds.reader import DefinitionsReader
-from suds.transport import TransportError, Request
-from suds.transport.https import HttpAuthenticated
-from suds.servicedefinition import ServiceDefinition
-from suds import sudsobject
-from sudsobject import Factory as InstFactory
-from sudsobject import Object
-from suds.resolver import PathResolver
+import suds.bindings.binding
 from suds.builder import Builder
-from suds.wsdl import Definitions
 from suds.cache import ObjectCache
-from suds.sax.document import Document
-from suds.sax.parser import Parser
+import suds.metrics as metrics
 from suds.options import Options
+from suds.plugin import PluginContainer
 from suds.properties import Unskin
-from urlparse import urlparse
+from suds.reader import DefinitionsReader
+from suds.resolver import PathResolver
+from suds.sax.document import Document
+from suds.sax.parser import Parser
+from suds.servicedefinition import ServiceDefinition
+from suds.transport import TransportError, Request
+from suds.transport.https import HttpAuthenticated
+from suds.umx.basic import Basic as UmxBasic
+from suds.wsdl import Definitions
+import sudsobject
+
+from cookielib import CookieJar
 from copy import deepcopy
-from suds.plugin import PluginContainer
-from logging import getLogger
+import httplib
+from urlparse import urlparse
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
-class Client(object):
-    """ 
+class Client(UnicodeMixin):
+    """
     A lightweight web services client.
     I{(2nd generation)} API.
     @ivar wsdl: The WSDL object.
@@ -72,7 +74,7 @@ class Client(object):
         @rtype: [(key, value),...]
         """
         return sudsobject.items(sobject)
-    
+
     @classmethod
     def dict(cls, sobject):
         """
@@ -84,7 +86,7 @@ class Client(object):
         @rtype: dict
         """
         return sudsobject.asdict(sobject)
-    
+
     @classmethod
     def metadata(cls, sobject):
         """
@@ -106,7 +108,8 @@ class Client(object):
         options = Options()
         options.transport = HttpAuthenticated()
         self.options = options
-        options.cache = ObjectCache(days=1)
+        if "cache" not in kwargs:
+            kwargs["cache"] = ObjectCache(days=1)
         self.set_options(**kwargs)
         reader = DefinitionsReader(options, Definitions)
         self.wsdl = reader.open(url)
@@ -119,7 +122,7 @@ class Client(object):
             sd = ServiceDefinition(self.wsdl, s)
             self.sd.append(sd)
         self.messages = dict(tx=None, rx=None)
-        
+
     def set_options(self, **kwargs):
         """
         Set options.
@@ -128,12 +131,12 @@ class Client(object):
         """
         p = Unskin(self.options)
         p.update(kwargs)
-        
+
     def add_prefix(self, prefix, uri):
         """
         Add I{static} mapping of an XML namespace prefix to a namespace.
         This is useful for cases when a wsdl and referenced schemas make heavy
-        use of namespaces and those namespaces are subject to changed.
+        use of namespaces and those namespaces are subject to change.
         @param prefix: An XML namespace prefix.
         @type prefix: str
         @param uri: An XML namespace URI.
@@ -147,23 +150,7 @@ class Client(object):
             return
         if mapped[1] != uri:
             raise Exception('"%s" already mapped as "%s"' % (prefix, mapped))
-        
-    def last_sent(self):
-        """
-        Get last sent I{soap} message.
-        @return: The last sent I{soap} message.
-        @rtype: L{Document}
-        """
-        return self.messages.get('tx')
-    
-    def last_received(self):
-        """
-        Get last received I{soap} message.
-        @return: The last received I{soap} message.
-        @rtype: L{Document}
-        """
-        return self.messages.get('rx')
-    
+
     def clone(self):
         """
         Get a shallow clone of this object.
@@ -186,16 +173,13 @@ class Client(object):
         clone.sd = self.sd
         clone.messages = dict(tx=None, rx=None)
         return clone
- 
-    def __str__(self):
-        return unicode(self)
-        
+
     def __unicode__(self):
         s = ['\n']
-        build = suds.__build__.split()
         s.append('Suds ( https://fedorahosted.org/suds/ )')
         s.append('  version: %s' % suds.__version__)
-        s.append(' %s  build: %s' % (build[0], build[1]))
+        if ( suds.__build__ ):
+            s.append('  build: %s' % suds.__build__)
         for sd in self.sd:
             s.append('\n\n%s' % unicode(sd))
         return ''.join(s)
@@ -209,7 +193,7 @@ class Factory:
     @ivar builder: A schema object builder.
     @type builder: L{Builder}
     """
-    
+
     def __init__(self, wsdl):
         """
         @param wsdl: A schema object.
@@ -218,7 +202,7 @@ class Factory:
         self.wsdl = wsdl
         self.resolver = PathResolver(wsdl)
         self.builder = Builder(self.resolver)
-    
+
     def create(self, name):
         """
         create a WSDL type by name
@@ -233,7 +217,7 @@ class Factory:
         if type is None:
             raise TypeNotFound(name)
         if type.enum():
-            result = InstFactory.object(name)
+            result = sudsobject.Factory.object(name)
             for e, a in type.children():
                 setattr(result, e.name, e.name)
         else:
@@ -245,7 +229,7 @@ class Factory:
         timer.stop()
         metrics.log.debug('%s created: %s', name, timer)
         return result
-    
+
     def separator(self, ps):
         """
         Set the path separator.
@@ -280,7 +264,7 @@ class ServiceSelector:
         """
         self.__client = client
         self.__services = services
-    
+
     def __getattr__(self, name):
         """
         Request to access an attribute is forwarded to the
@@ -289,7 +273,7 @@ class ServiceSelector:
         @param name: The name of a method.
         @type name: str
         @return: A L{PortSelector}.
-        @rtype: L{PortSelector}. 
+        @rtype: L{PortSelector}.
         """
         default = self.__ds()
         if default is None:
@@ -297,17 +281,17 @@ class ServiceSelector:
         else:
             port = default
         return getattr(port, name)
-    
+
     def __getitem__(self, name):
         """
-        Provides selection of the I{service} by name (string) or 
+        Provides selection of the I{service} by name (string) or
         index (integer).  In cases where only (1) service is defined
         or a I{default} has been specified, the request is forwarded
         to the L{PortSelector}.
         @param name: The name (or index) of a service.
         @type name: (int|str)
         @return: A L{PortSelector} for the specified service.
-        @rtype: L{PortSelector}. 
+        @rtype: L{PortSelector}.
         """
         if len(self.__services) == 1:
             port = self.__find(0)
@@ -317,14 +301,14 @@ class ServiceSelector:
             port = default
             return port[name]
         return self.__find(name)
-    
+
     def __find(self, name):
         """
         Find a I{service} by name (string) or index (integer).
         @param name: The name (or index) of a service.
         @type name: (int|str)
         @return: A L{PortSelector} for the found service.
-        @rtype: L{PortSelector}. 
+        @rtype: L{PortSelector}.
         """
         service = None
         if not len(self.__services):
@@ -343,12 +327,12 @@ class ServiceSelector:
         if service is None:
             raise ServiceNotFound, name
         return PortSelector(self.__client, service.ports, name)
-    
+
     def __ds(self):
         """
         Get the I{default} service if defined in the I{options}.
         @return: A L{PortSelector} for the I{default} service.
-        @rtype: L{PortSelector}. 
+        @rtype: L{PortSelector}.
         """
         ds = self.__client.options.service
         if ds is None:
@@ -384,7 +368,7 @@ class PortSelector:
         self.__client = client
         self.__ports = ports
         self.__qn = qn
-    
+
     def __getattr__(self, name):
         """
         Request to access an attribute is forwarded to the
@@ -393,7 +377,7 @@ class PortSelector:
         @param name: The name of a method.
         @type name: str
         @return: A L{MethodSelector}.
-        @rtype: L{MethodSelector}. 
+        @rtype: L{MethodSelector}.
         """
         default = self.__dp()
         if default is None:
@@ -401,31 +385,31 @@ class PortSelector:
         else:
             m = default
         return getattr(m, name)
-    
+
     def __getitem__(self, name):
         """
-        Provides selection of the I{port} by name (string) or 
+        Provides selection of the I{port} by name (string) or
         index (integer).  In cases where only (1) port is defined
         or a I{default} has been specified, the request is forwarded
         to the L{MethodSelector}.
         @param name: The name (or index) of a port.
         @type name: (int|str)
         @return: A L{MethodSelector} for the specified port.
-        @rtype: L{MethodSelector}. 
+        @rtype: L{MethodSelector}.
         """
         default = self.__dp()
         if default is None:
             return self.__find(name)
         else:
             return default
-    
+
     def __find(self, name):
         """
         Find a I{port} by name (string) or index (integer).
         @param name: The name (or index) of a port.
         @type name: (int|str)
         @return: A L{MethodSelector} for the found port.
-        @rtype: L{MethodSelector}. 
+        @rtype: L{MethodSelector}.
         """
         port = None
         if not len(self.__ports):
@@ -446,19 +430,19 @@ class PortSelector:
             raise PortNotFound, qn
         qn = '.'.join((self.__qn, port.name))
         return MethodSelector(self.__client, port.methods, qn)
-    
+
     def __dp(self):
         """
         Get the I{default} port if defined in the I{options}.
         @return: A L{MethodSelector} for the I{default} port.
-        @rtype: L{MethodSelector}. 
+        @rtype: L{MethodSelector}.
         """
         dp = self.__client.options.port
         if dp is None:
             return None
         else:
             return self.__find(dp)
-    
+
 
 class MethodSelector:
     """
@@ -482,7 +466,7 @@ class MethodSelector:
         self.__client = client
         self.__methods = methods
         self.__qn = qn
-    
+
     def __getattr__(self, name):
         """
         Get a method by name and return it in an I{execution wrapper}.
@@ -492,7 +476,7 @@ class MethodSelector:
         @rtype: L{Method}
         """
         return self[name]
-    
+
     def __getitem__(self, name):
         """
         Get a method by name and return it in an I{execution wrapper}.
@@ -526,31 +510,29 @@ class Method:
         """
         self.client = client
         self.method = method
-    
+
     def __call__(self, *args, **kwargs):
         """
         Invoke the method.
         """
         clientclass = self.clientclass(kwargs)
         client = clientclass(self.client, self.method)
-        if not self.faults():
-            try:
-                return client.invoke(args, kwargs)
-            except WebFault, e:
-                return (500, e)
-        else:
+        try:
             return client.invoke(args, kwargs)
-        
+        except WebFault, e:
+            if self.faults():
+                raise
+            return (httplib.INTERNAL_SERVER_ERROR, e)
+
     def faults(self):
         """ get faults option """
         return self.client.options.faults
-        
+
     def clientclass(self, kwargs):
         """ get soap client class """
         if SimClient.simulation(kwargs):
             return SimClient
-        else:
-            return SoapClient
+        return SoapClient
 
 
 class SoapClient:
@@ -577,7 +559,7 @@ class SoapClient:
         self.method = method
         self.options = client.options
         self.cookiejar = CookieJar()
-        
+
     def invoke(self, args, kwargs):
         """
         Send the required soap message to invoke the specified method
@@ -590,23 +572,17 @@ class SoapClient:
         """
         timer = metrics.Timer()
         timer.start()
-        result = None
         binding = self.method.binding.input
         soapenv = binding.get_message(self.method, args, kwargs)
         timer.stop()
-        metrics.log.debug(
-                "message for '%s' created: %s",
-                self.method.name,
-                timer)
+        metrics.log.debug("message for '%s' created: %s", self.method.name,
+            timer)
         timer.start()
         result = self.send(soapenv)
         timer.stop()
-        metrics.log.debug(
-                "method '%s' invoked: %s",
-                self.method.name,
-                timer)
+        metrics.log.debug("method '%s' invoked: %s", self.method.name, timer)
         return result
-    
+
     def send(self, soapenv):
         """
         Send soap message.
@@ -615,139 +591,159 @@ class SoapClient:
         @return: The reply to the sent message.
         @rtype: I{builtin} or I{subclass of} L{Object}
         """
-        result = None
         location = self.location()
-        binding = self.method.binding.input
-        transport = self.options.transport
-        retxml = self.options.retxml
-        nosend = self.options.nosend
-        prettyxml = self.options.prettyxml
         log.debug('sending to (%s)\nmessage:\n%s', location, soapenv)
+        original_soapenv = soapenv
+        plugins = PluginContainer(self.options.plugins)
+        plugins.message.marshalled(envelope=soapenv.root())
+        if self.options.prettyxml:
+            soapenv = soapenv.str()
+        else:
+            soapenv = soapenv.plain()
+        soapenv = soapenv.encode('utf-8')
+        ctx = plugins.message.sending(envelope=soapenv)
+        soapenv = ctx.envelope
+        if self.options.nosend:
+            return RequestContext(self, soapenv, original_soapenv)
+        request = Request(location, soapenv)
+        request.headers = self.headers()
         try:
-            self.last_sent(soapenv)
-            plugins = PluginContainer(self.options.plugins)
-            plugins.message.marshalled(envelope=soapenv.root())
-            if prettyxml:
-                soapenv = soapenv.str()
-            else:
-                soapenv = soapenv.plain()
-            soapenv = soapenv.encode('utf-8')
-            ctx = plugins.message.sending(envelope=soapenv)
-            soapenv = ctx.envelope
-            if nosend:
-                return RequestContext(self, binding, soapenv)
-            request = Request(location, soapenv)
-            request.headers = self.headers()
-            reply = transport.send(request)
-            ctx = plugins.message.received(reply=reply.message)
-            reply.message = ctx.reply
-            if retxml:
-                result = reply.message
-            else:
-                result = self.succeeded(binding, reply.message)
+            timer = metrics.Timer()
+            timer.start()
+            reply = self.options.transport.send(request)
+            timer.stop()
+            metrics.log.debug('waited %s on server reply', timer)
         except TransportError, e:
-            if e.httpcode in (202,204):
-                result = None
+            content = e.fp and e.fp.read() or ''
+            return self.process_reply(reply=content, status=e.httpcode,
+                description=tostr(e), original_soapenv=original_soapenv)
+        return self.process_reply(reply=reply.message,
+            original_soapenv=original_soapenv)
+
+    def process_reply(self, reply, status=None, description=None,
+        original_soapenv=None):
+        if status is None:
+            status = httplib.OK
+        if status in (httplib.ACCEPTED, httplib.NO_CONTENT):
+            return
+        failed = True
+        try:
+            if status == httplib.OK:
+                log.debug('HTTP succeeded:\n%s', reply)
             else:
-                log.error(self.last_sent())
-                result = self.failed(binding, e)
-        return result
-    
+                log.debug('HTTP failed - %d - %s:\n%s', status, description,
+                    reply)
+
+            # (todo)
+            #   Consider whether and how to allow plugins to handle error,
+            # httplib.ACCEPTED & httplib.NO_CONTENT replies as well as
+            # successful ones.
+            #                                 (todo) (27.03.2013.) (Jurko)
+            plugins = PluginContainer(self.options.plugins)
+            ctx = plugins.message.received(reply=reply)
+            reply = ctx.reply
+
+            # SOAP standard states that SOAP errors must be accompanied by HTTP
+            # status code 500 - internal server error:
+            #
+            # From SOAP 1.1 Specification:
+            #   In case of a SOAP error while processing the request, the SOAP
+            # HTTP server MUST issue an HTTP 500 "Internal Server Error"
+            # response and include a SOAP message in the response containing a
+            # SOAP Fault element (see section 4.4) indicating the SOAP
+            # processing error.
+            #
+            # From WS-I Basic profile:
+            #   An INSTANCE MUST use a "500 Internal Server Error" HTTP status
+            # code if the response message is a SOAP Fault.
+            replyroot = None
+            if status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+                replyroot = _parse(reply)
+                plugins.message.parsed(reply=replyroot)
+                fault = self.get_fault(replyroot)
+                if fault:
+                    if status != httplib.INTERNAL_SERVER_ERROR:
+                        log.warn("Web service reported a SOAP processing "
+                            "fault using an unexpected HTTP status code %d. "
+                            "Reporting as an internal server error.", status)
+                    if self.options.faults:
+                        raise WebFault(fault, replyroot)
+                    return (httplib.INTERNAL_SERVER_ERROR, fault)
+            if status != httplib.OK:
+                if self.options.faults:
+                    # (todo)
+                    #   Use a more specific exception class here.
+                    #                         (27.03.2013.) (Jurko)
+                    raise Exception((status, description))
+                return (status, description)
+
+            if self.options.retxml:
+                failed = False
+                return reply
+
+            result = replyroot and self.method.binding.output.get_reply(
+                self.method, replyroot)
+            ctx = plugins.message.unmarshalled(reply=result)
+            result = ctx.reply
+            failed = False
+            if self.options.faults:
+                return result
+            return (httplib.OK, result)
+        finally:
+            if failed and original_soapenv:
+                log.error(original_soapenv)
+
+    def get_fault(self, replyroot):
+        """Extract fault information from the specified SOAP reply.
+
+          Returns an I{unmarshalled} fault L{Object} or None in case the given
+        XML document does not contain the SOAP <Fault> element.
+
+        @param replyroot: A SOAP reply message root XML element or None.
+        @type replyroot: L{Element}
+        @return: A fault object.
+        @rtype: L{Object}
+        """
+        envns = suds.bindings.binding.envns
+        soapenv = replyroot and replyroot.getChild('Envelope', envns)
+        soapbody = soapenv and soapenv.getChild('Body', envns)
+        fault = soapbody and soapbody.getChild('Fault', envns)
+        return fault is not None and UmxBasic().process(fault)
+
     def headers(self):
         """
-        Get http headers or the http/https request.
+        Get HTTP headers or the HTTP/HTTPS request.
         @return: A dictionary of header/values.
         @rtype: dict
         """
         action = self.method.soap.action
         if isinstance(action, unicode):
             action = action.encode('utf-8')
-        stock = { 'Content-Type' : 'text/xml; charset=utf-8', 'SOAPAction': action }
+        stock = {'Content-Type':'text/xml; charset=utf-8', 'SOAPAction':action}
         result = dict(stock, **self.options.headers)
         log.debug('headers = %s', result)
         return result
-    
-    def succeeded(self, binding, reply):
-        """
-        Request succeeded, process the reply
-        @param binding: The binding to be used to process the reply.
-        @type binding: L{bindings.binding.Binding}
-        @param reply: The raw reply text.
-        @type reply: str
-        @return: The method result.
-        @rtype: I{builtin}, L{Object}
-        @raise WebFault: On server.
-        """
-        log.debug('http succeeded:\n%s', reply)
-        plugins = PluginContainer(self.options.plugins)
-        if len(reply) > 0:
-            reply, result = binding.get_reply(self.method, reply)
-            self.last_received(reply)
-        else:
-            result = None
-        ctx = plugins.message.unmarshalled(reply=result)
-        result = ctx.reply
-        if self.options.faults:
-            return result
-        else:
-            return (200, result)
-        
-    def failed(self, binding, error):
-        """
-        Request failed, process reply based on reason
-        @param binding: The binding to be used to process the reply.
-        @type binding: L{suds.bindings.binding.Binding}
-        @param error: The http error message
-        @type error: L{transport.TransportError}
-        """
-        status, reason = (error.httpcode, tostr(error))
-        reply = error.fp.read()
-        log.debug('http failed:\n%s', reply)
-        if status == 500:
-            if len(reply) > 0:
-                r, p = binding.get_fault(reply)
-                self.last_received(r)
-                return (status, p)
-            else:
-                return (status, None)
-        if self.options.faults:
-            raise Exception((status, reason))
-        else:
-            return (status, None)
 
     def location(self):
-        p = Unskin(self.options)
-        return p.get('location', self.method.location)
-    
-    def last_sent(self, d=None):
-        key = 'tx'
-        messages = self.client.messages
-        if d is None:
-            return messages.get(key)
-        else:
-            messages[key] = d
-        
-    def last_received(self, d=None):
-        key = 'rx'
-        messages = self.client.messages
-        if d is None:
-            return messages.get(key)
-        else:
-            messages[key] = d
+        """
+        Returns the SOAP request's target location URL.
+
+        """
+        return Unskin(self.options).get('location', self.method.location)
 
 
 class SimClient(SoapClient):
     """
     Loopback client used for message/reply simulation.
     """
-    
+
     injkey = '__inject'
-    
+
     @classmethod
     def simulation(cls, kwargs):
         """ get whether loopback has been specified in the I{kwargs}. """
         return kwargs.has_key(SimClient.injkey)
-        
+
     def invoke(self, args, kwargs):
         """
         Send the required soap message to invoke the specified method
@@ -760,81 +756,77 @@ class SimClient(SoapClient):
         """
         simulation = kwargs[self.injkey]
         msg = simulation.get('msg')
-        reply = simulation.get('reply')
-        fault = simulation.get('fault')
-        if msg is None:
-            if reply is not None:
-                return self.__reply(reply, args, kwargs)
-            if fault is not None:
-                return self.__fault(fault)
-            raise Exception('(reply|fault) expected when msg=None')
-        sax = Parser()
-        msg = sax.parse(string=msg)
-        return self.send(msg)
-    
-    def __reply(self, reply, args, kwargs):
-        """ simulate the reply """
-        binding = self.method.binding.input
-        msg = binding.get_message(self.method, args, kwargs)
+        if msg is not None:
+            assert msg.__class__ is suds.byte_str_class
+            return self.send(_parse(msg))
+        msg = self.method.binding.input.get_message(self.method, args, kwargs)
         log.debug('inject (simulated) send message:\n%s', msg)
-        binding = self.method.binding.output
-        return self.succeeded(binding, reply)
-    
-    def __fault(self, reply):
-        """ simulate the (fault) reply """
-        binding = self.method.binding.output
-        if self.options.faults:
-            r, p = binding.get_fault(reply)
-            self.last_received(r)
-            return (500, p)
-        else:
-            return (500, None)
-        
+        reply = simulation.get('reply')
+        if reply is not None:
+            assert reply.__class__ is suds.byte_str_class
+            status = simulation.get('status')
+            description=simulation.get('description')
+            if description is None:
+                description = 'injected reply'
+            return self.process_reply(reply=reply, status=status,
+                description=description, original_soapenv=msg)
+        raise Exception('reply or msg injection parameter expected');
+
 
 class RequestContext:
     """
     A request context.
-    Returned when the ''nosend'' options is specified.
+    Returned when the ''nosend'' options is specified. Allows the caller to
+    take care of sending the request himself and simply return the reply data
+    for further processing.
     @ivar client: The suds client.
     @type client: L{Client}
-    @ivar binding: The binding for this request.
-    @type binding: I{Binding}
-    @ivar envelope: The request soap envelope.
+    @ivar envelope: The request SOAP envelope.
     @type envelope: str
+    @ivar original_envelope: The original request SOAP envelope before plugin
+                             processing.
+    @type original_envelope: str
     """
-    
-    def __init__(self, client, binding, envelope):
+
+    def __init__(self, client, envelope, original_envelope):
         """
         @param client: The suds client.
         @type client: L{Client}
-        @param binding: The binding for this request.
-        @type binding: I{Binding}
-        @param envelope: The request soap envelope.
+        @param envelope: The request SOAP envelope.
         @type envelope: str
+        @param original_envelope: The original request SOAP envelope before
+                                  plugin processing.
+        @type original_envelope: str
         """
         self.client = client
-        self.binding = binding
         self.envelope = envelope
-        
-    def succeeded(self, reply):
+        self.original_envelope = original_envelope
+
+    def process_reply(self, reply, status=None, description=None):
         """
         Re-entry for processing a successful reply.
-        @param reply: The reply soap envelope.
+        @param reply: The reply SOAP envelope.
         @type reply: str
+        @param status: The HTTP status code
+        @type status: int
+        @param description: Additional status description.
+        @type description: str
         @return: The returned value for the invoked method.
-        @rtype: object 
+        @return: The result of the method invocation.
+        @rtype: I{builtin}|I{subclass of} L{Object}
         """
-        options = self.client.options
-        plugins = PluginContainer(options.plugins)
-        ctx = plugins.message.received(reply=reply)
-        reply = ctx.reply
-        return self.client.succeeded(self.binding, reply)
-    
-    def failed(self, error):
-        """
-        Re-entry for processing a failure reply.
-        @param error: The error returned by the transport.
-        @type error: A suds I{TransportError}.
-        """
-        return self.client.failed(self.binding, error)
-        
\ No newline at end of file
+        return self.client.process_reply(reply=reply, status=status,
+            description=description, original_soapenv=self.original_envelope)
+
+
+def _parse(string):
+    """
+    Parses the given XML document content and returns the resulting root XML
+    element node. Returns None if the given XML content is empty.
+    @param string: XML document content to parse.
+    @type string: str
+    @return: Resulting root XML element node or None.
+    @rtype: L{Element}
+    """
+    if len(string) > 0:
+        return Parser().parse(string=string)
diff --git a/suds/metrics.py b/suds/metrics.py
index 403224a..9b15f18 100644
--- a/suds/metrics.py
+++ b/suds/metrics.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -20,12 +20,13 @@ designed for collecting and reporting performance metrics.
 """
 
 import time
-from logging import getLogger
 from suds import *
 from math import modf
 
+from logging import getLogger
 log = getLogger(__name__)
 
+
 class Timer:
 
     def __init__(self):
@@ -54,7 +55,7 @@ class Timer:
         jmod = ( lambda m : (m[1], m[0]*1000) )
         if duration < 1:
             ms = (duration*1000)
-            return '%d (ms)' % ms           
+            return '%d (ms)' % ms
         if duration < 60:
             m = modf(duration)
             return '%d.%.3d (seconds)' % jmod(m)
diff --git a/suds/mx/__init__.py b/suds/mx/__init__.py
index 77e6ca1..719e52d 100644
--- a/suds/mx/__init__.py
+++ b/suds/mx/__init__.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -30,9 +30,9 @@ class Content(Object):
     @ivar value: The content's value.
     @type value: I{any}
     """
-    
+
     extensions = []
-    
+
     def __init__(self, tag=None, value=None, **kwargs):
         """
         @param tag: The content tag.
@@ -45,7 +45,7 @@ class Content(Object):
         self.value = value
         for k,v in kwargs.items():
             setattr(self, k, v)
-            
+
     def __getattr__(self, name):
         if name not in self.__dict__:
             if name in self.extensions:
@@ -56,4 +56,4 @@ class Content(Object):
                     'Content has no attribute %s' % name
         else:
             v = self.__dict__[name]
-        return v
\ No newline at end of file
+        return v
diff --git a/suds/mx/appender.py b/suds/mx/appender.py
index 206abc0..f900338 100644
--- a/suds/mx/appender.py
+++ b/suds/mx/appender.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,16 +18,13 @@
 Provides appender classes for I{marshalling}.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.sudsobject import footprint
 from suds.sudsobject import Object, Property
 from suds.sax.element import Element
 from suds.sax.text import Text
-from copy import deepcopy
 
-log = getLogger(__name__)
 
 class Matcher:
     """
@@ -45,9 +42,8 @@ class Matcher:
 
     def __eq__(self, x):
         if self.cls is None:
-            return ( x is None )
-        else:
-            return isinstance(x, self.cls)
+            return x is None
+        return isinstance(x, self.cls)
 
 
 class ContentAppender:
@@ -66,26 +62,16 @@ class ContentAppender:
         """
         self.default = PrimativeAppender(marshaller)
         self.appenders = (
-            (Matcher(None),
-                NoneAppender(marshaller)),
-            (Matcher(null),
-                NoneAppender(marshaller)),
-            (Matcher(Property),
-                PropertyAppender(marshaller)),
-            (Matcher(Object),
-                ObjectAppender(marshaller)),
-            (Matcher(Element), 
-                ElementAppender(marshaller)),
-            (Matcher(Text), 
-                TextAppender(marshaller)),
-            (Matcher(list), 
-                ListAppender(marshaller)),
-            (Matcher(tuple), 
-                ListAppender(marshaller)),
-            (Matcher(dict), 
-                DictAppender(marshaller)),
-        )
-        
+            (Matcher(None), NoneAppender(marshaller)),
+            (Matcher(null), NoneAppender(marshaller)),
+            (Matcher(Property), PropertyAppender(marshaller)),
+            (Matcher(Object), ObjectAppender(marshaller)),
+            (Matcher(Element), ElementAppender(marshaller)),
+            (Matcher(Text), TextAppender(marshaller)),
+            (Matcher(list), ListAppender(marshaller)),
+            (Matcher(tuple), ListAppender(marshaller)),
+            (Matcher(dict), DictAppender(marshaller)))
+
     def append(self, parent, content):
         """
         Select an appender and append the content to parent.
@@ -95,9 +81,9 @@ class ContentAppender:
         @type content: L{Content}
         """
         appender = self.default
-        for a in self.appenders:
-            if a[0] == content.value:
-                appender = a[1]
+        for matcher, candidate_appender in self.appenders:
+            if matcher == content.value:
+                appender = candidate_appender
                 break
         appender.append(parent, content)
 
@@ -108,47 +94,47 @@ class Appender:
     @ivar marshaller: A marshaller.
     @type marshaller: L{suds.mx.core.Core}
     """
-    
+
     def __init__(self, marshaller):
         """
         @param marshaller: A marshaller.
         @type marshaller: L{suds.mx.core.Core}
         """
         self.marshaller  = marshaller
-        
+
     def node(self, content):
         """
         Create and return an XML node that is qualified
         using the I{type}.  Also, make sure all referenced namespace
         prefixes are declared.
-        @param content: The content for which proccessing has ended.
+        @param content: The content for which processing has ended.
         @type content: L{Object}
         @return: A new node.
         @rtype: L{Element}
         """
         return self.marshaller.node(content)
-    
+
     def setnil(self, node, content):
         """
         Set the value of the I{node} to nill.
         @param node: A I{nil} node.
         @type node: L{Element}
-        @param content: The content for which proccessing has ended.
+        @param content: The content for which processing has ended.
         @type content: L{Object}
         """
         self.marshaller.setnil(node, content)
-        
+
     def setdefault(self, node, content):
         """
         Set the value of the I{node} to a default value.
         @param node: A I{nil} node.
         @type node: L{Element}
-        @param content: The content for which proccessing has ended.
+        @param content: The content for which processing has ended.
         @type content: L{Object}
         @return: The default.
         """
         return self.marshaller.setdefault(node, content)
-    
+
     def optional(self, content):
         """
         Get whether the specified content is optional.
@@ -156,23 +142,23 @@ class Appender:
         @type content: L{Content}
         """
         return self.marshaller.optional(content)
-        
+
     def suspend(self, content):
         """
         Notify I{marshaller} that appending this content has suspended.
-        @param content: The content for which proccessing has been suspended.
+        @param content: The content for which processing has been suspended.
         @type content: L{Object}
         """
         self.marshaller.suspend(content)
-        
+
     def resume(self, content):
         """
         Notify I{marshaller} that appending this content has resumed.
-        @param content: The content for which proccessing has been resumed.
+        @param content: The content for which processing has been resumed.
         @type content: L{Object}
         """
         self.marshaller.resume(content)
-    
+
     def append(self, parent, content):
         """
         Append the specified L{content} to the I{parent}.
@@ -181,12 +167,12 @@ class Appender:
         """
         self.marshaller.append(parent, content)
 
-       
+
 class PrimativeAppender(Appender):
     """
     An appender for python I{primative} types.
     """
-        
+
     def append(self, parent, content):
         if content.tag.startswith('_'):
             attr = content.tag[1:]
@@ -203,7 +189,7 @@ class NoneAppender(Appender):
     """
     An appender for I{None} values.
     """
-        
+
     def append(self, parent, content):
         child = self.node(content)
         default = self.setdefault(child, content)
@@ -216,7 +202,7 @@ class PropertyAppender(Appender):
     """
     A L{Property} appender.
     """
-        
+
     def append(self, parent, content):
         p = content.value
         child = self.node(content)
@@ -226,12 +212,12 @@ class PropertyAppender(Appender):
             cont = Content(tag=item[0], value=item[1])
             Appender.append(self, child, cont)
 
-            
+
 class ObjectAppender(Appender):
     """
     An L{Object} appender.
     """
-        
+
     def append(self, parent, content):
         object = content.value
         if self.optional(content) and footprint(object) == 0:
@@ -241,13 +227,13 @@ class ObjectAppender(Appender):
         for item in object:
             cont = Content(tag=item[0], value=item[1])
             Appender.append(self, child, cont)
-            
+
 
 class DictAppender(Appender):
     """
     An python I{dict} appender.
     """
-        
+
     def append(self, parent, content):
         d = content.value
         if self.optional(content) and len(d) == 0:
@@ -257,17 +243,17 @@ class DictAppender(Appender):
         for item in d.items():
             cont = Content(tag=item[0], value=item[1])
             Appender.append(self, child, cont)
-            
+
 
 class ElementWrapper(Element):
     """
     Element wrapper.
     """
-    
+
     def __init__(self, content):
         Element.__init__(self, content.name, content.parent)
         self.__content = content
-        
+
     def str(self, indent=0):
         return self.__content.str(indent)
 
diff --git a/suds/mx/basic.py b/suds/mx/basic.py
index 336f684..b2de161 100644
--- a/suds/mx/basic.py
+++ b/suds/mx/basic.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,19 +18,16 @@
 Provides basic I{marshaller} classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.mx.core import Core
 
-log = getLogger(__name__)
-
 
 class Basic(Core):
     """
     A I{basic} (untyped) marshaller.
     """
-    
+
     def process(self, value, tag=None):
         """
         Process (marshal) the tag with the specified value using the
@@ -45,4 +42,4 @@ class Basic(Core):
         """
         content = Content(tag=tag, value=value)
         result = Core.process(self, content)
-        return result
\ No newline at end of file
+        return result
diff --git a/suds/mx/core.py b/suds/mx/core.py
index 3c9ef59..b7fe24c 100644
--- a/suds/mx/core.py
+++ b/suds/mx/core.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,7 +18,6 @@
 Provides I{marshaller} core classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.mx.appender import ContentAppender
@@ -26,7 +25,7 @@ from suds.sax.element import Element
 from suds.sax.document import Document
 from suds.sudsobject import Property
 
-
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -57,11 +56,9 @@ class Core:
         document = Document()
         if isinstance(content.value, Property):
             root = self.node(content)
-            self.append(document, content)
-        else:
-            self.append(document, content)
+        self.append(document, content)
         return document.root()
-    
+
     def append(self, parent, content):
         """
         Append the specified L{content} to the I{parent}.
@@ -84,35 +81,35 @@ class Core:
     def node(self, content):
         """
         Create and return an XML node.
-        @param content: The content for which proccessing has been suspended.
+        @param content: The content for which processing has been suspended.
         @type content: L{Object}
         @return: An element.
         @rtype: L{Element}
         """
         return Element(content.tag)
-    
+
     def start(self, content):
         """
         Appending this content has started.
-        @param content: The content for which proccessing has started.
+        @param content: The content for which processing has started.
         @type content: L{Content}
         @return: True to continue appending
         @rtype: boolean
         """
         return True
-    
+
     def suspend(self, content):
         """
         Appending this content has suspended.
-        @param content: The content for which proccessing has been suspended.
+        @param content: The content for which processing has been suspended.
         @type content: L{Content}
         """
         pass
-    
+
     def resume(self, content):
         """
         Appending this content has resumed.
-        @param content: The content for which proccessing has been resumed.
+        @param content: The content for which processing has been resumed.
         @type content: L{Content}
         """
         pass
@@ -122,11 +119,11 @@ class Core:
         Appending this content has ended.
         @param parent: The parent node ending.
         @type parent: L{Element}
-        @param content: The content for which proccessing has ended.
+        @param content: The content for which processing has ended.
         @type content: L{Content}
         """
         pass
-    
+
     def setnil(self, node, content):
         """
         Set the value of the I{node} to nill.
@@ -147,7 +144,7 @@ class Core:
         @return: The default.
         """
         pass
-    
+
     def optional(self, content):
         """
         Get whether the specified content is optional.
@@ -155,4 +152,3 @@ class Core:
         @type content: L{Content}
         """
         return False
-
diff --git a/suds/mx/encoded.py b/suds/mx/encoded.py
index 9cbc8c5..ec09536 100644
--- a/suds/mx/encoded.py
+++ b/suds/mx/encoded.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,7 +18,6 @@
 Provides encoded I{marshaller} classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.mx.literal import Literal
@@ -26,7 +25,6 @@ from suds.mx.typer import Typer
 from suds.sudsobject import Factory, Object
 from suds.xsd.query import TypeQuery
 
-log = getLogger(__name__)
 
 #
 # Add encoded extensions
@@ -40,13 +38,13 @@ class Encoded(Literal):
     A SOAP section (5) encoding marshaller.
     This marshaller supports rpc/encoded soap styles.
     """
-    
+
     def start(self, content):
         #
         # For soap encoded arrays, the 'aty' (array type) information
         # is extracted and added to the 'content'.  Then, the content.value
         # is replaced with an object containing an 'item=[]' attribute
-        # containing values that are 'typed' suds objects. 
+        # containing values that are 'typed' suds objects.
         #
         start = Literal.start(self, content)
         if start and isinstance(content.value, (list,tuple)):
@@ -57,7 +55,7 @@ class Encoded(Literal):
                     self.cast(content)
                     break
         return start
-    
+
     def end(self, parent, content):
         #
         # For soap encoded arrays, the soapenc:arrayType attribute is
@@ -75,9 +73,9 @@ class Encoded(Literal):
         child.addPrefix(ns0[0], ns0[1])
         child.addPrefix(ns1[0], ns1[1])
         name = '%s:arrayType' % ns1[0]
-        value = '%s:%s[%d]' % (ns0[0], aty[0], len(array)) 
+        value = '%s:%s[%d]' % (ns0[0], aty[0], len(array))
         child.set(name, value)
-        
+
     def encode(self, node, content):
         if content.type.any():
             Typer.auto(node, content.value)
@@ -90,7 +88,7 @@ class Encoded(Literal):
         if self.xstq:
             ns = content.real.namespace()
         Typer.manual(node, name, ns)
-        
+
     def cast(self, content):
         """
         Cast the I{untyped} list items found in content I{value}.
@@ -117,13 +115,13 @@ class Encoded(Literal):
             if isinstance(x, Object):
                 md = x.__metadata__
                 md.sxtype = ref
-                array.item.append(x) 
+                array.item.append(x)
                 continue
             if isinstance(x, dict):
                 x = Factory.object(ref.name, x)
                 md = x.__metadata__
                 md.sxtype = ref
-                array.item.append(x) 
+                array.item.append(x)
                 continue
             x = Factory.property(ref.name, x)
             md = x.__metadata__
diff --git a/suds/mx/literal.py b/suds/mx/literal.py
index 937ad8e..4b3480f 100644
--- a/suds/mx/literal.py
+++ b/suds/mx/literal.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,7 +18,6 @@
 Provides literal I{marshaller} classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.mx.core import Core
@@ -27,6 +26,7 @@ from suds.resolver import GraphResolver, Frame
 from suds.sax.element import Element
 from suds.sudsobject import Factory
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -41,12 +41,11 @@ Content.extensions.append('real')
 Content.extensions.append('ancestry')
 
 
-
 class Typed(Core):
     """
     A I{typed} marshaller.
-    This marshaller is semi-typed as needed to support both
-    I{document/literal} and I{rpc/literal} soap message styles.
+    This marshaller is semi-typed as needed to support both I{document/literal}
+    and I{rpc/literal} soap message styles.
     @ivar schema: An xsd schema.
     @type schema: L{xsd.schema.Schema}
     @ivar resolver: A schema type resolver.
@@ -58,30 +57,30 @@ class Typed(Core):
         @param schema: A schema object
         @type schema: L{xsd.schema.Schema}
         @param xstq: The B{x}ml B{s}chema B{t}ype B{q}ualified flag indicates
-            that the I{xsi:type} attribute values should be qualified by namespace.
+            that the I{xsi:type} attribute values should be qualified by
+            namespace.
         @type xstq: bool
         """
         Core.__init__(self)
         self.schema = schema
         self.xstq = xstq
         self.resolver = GraphResolver(self.schema)
-    
+
     def reset(self):
         self.resolver.reset()
-            
+
     def start(self, content):
         #
-        # Start marshalling the 'content' by ensuring that both the
-        # 'content' _and_ the resolver are primed with the XSD type
-        # information.  The 'content' value is both translated and
-        # sorted based on the XSD type.  Only values that are objects
-        # have their attributes sorted.
+        # Start marshalling the 'content' by ensuring that both the 'content'
+        # _and_ the resolver are primed with the XSD type information. The
+        # 'content' value is both translated and sorted based on the XSD type.
+        # Only values that are objects have their attributes sorted.
         #
         log.debug('starting content:\n%s', content)
         if content.type is None:
             name = content.tag
             if name.startswith('_'):
-                name = '@'+name[1:]
+                name = '@' + name[1:]
             content.type = self.resolver.find(name, content.value)
             if content.type is None:
                 raise TypeNotFound(content.tag)
@@ -90,7 +89,8 @@ class Typed(Core):
             if isinstance(content.value, Object):
                 known = self.resolver.known(content.value)
                 if known is None:
-                    log.debug('object has no type information', content.value)
+                    log.debug('object %s has no type information',
+                        content.value)
                     known = content.type
             frame = Frame(content.type, resolved=known)
             self.resolver.push(frame)
@@ -103,30 +103,28 @@ class Typed(Core):
             log.debug('skipping (optional) content:\n%s', content)
             self.resolver.pop()
             return False
-        else:
-            return True
-        
+        return True
+
     def suspend(self, content):
         #
-        # Suspend to process a list content.  Primarily, this
-        # involves popping the 'list' content off the resolver's
-        # stack so the list items can be marshalled.
+        # Suspend to process a list content. Primarily, this involves popping
+        # the 'list' content off the resolver's stack its list items can be
+        # marshalled.
         #
         self.resolver.pop()
-    
+
     def resume(self, content):
         #
-        # Resume processing a list content.  To do this, we
-        # really need to simply push the 'list' content
-        # back onto the resolver stack.
+        # Resume processing a list content. To do this, we really need to
+        # simply push the 'list' content back onto the resolver stack.
         #
         self.resolver.push(Frame(content.type))
-        
+
     def end(self, parent, content):
         #
-        # End processing the content.  Make sure the content
-        # ending matches the top of the resolver stack since for
-        # list processing we play games with the resolver stack.
+        # End processing the content. Make sure the content ending matches the
+        # top of the resolver stack since for list processing we play games
+        # with the resolver stack.
         #
         log.debug('ending content:\n%s', content)
         current = self.resolver.top().type
@@ -136,42 +134,40 @@ class Typed(Core):
             raise Exception, \
                 'content (end) mismatch: top=(%s) cont=(%s)' % \
                 (current, content)
-    
+
     def node(self, content):
         #
-        # Create an XML node and namespace qualify as defined
-        # by the schema (elementFormDefault).
+        # Create an XML node and namespace qualify as defined by the schema
+        # (elementFormDefault).
         #
         ns = content.type.namespace()
         if content.type.form_qualified:
             node = Element(content.tag, ns=ns)
-            node.addPrefix(ns[0], ns[1])
+            if ns[0]:
+                node.addPrefix(ns[0], ns[1])
         else:
             node = Element(content.tag)
         self.encode(node, content)
         log.debug('created - node:\n%s', node)
         return node
-    
+
     def setnil(self, node, content):
         #
-        # Set the 'node' nil only if the XSD type
-        # specifies that it is permitted.
+        # Set the 'node' nil only if the XSD type specifies that it is
+        # permitted.
         #
         if content.type.nillable:
             node.setnil()
-            
+
     def setdefault(self, node, content):
         #
-        # Set the node to the default value specified
-        # by the XSD type.
+        # Set the node to the default value specified by the XSD type.
         #
         default = content.type.default
-        if default is None:
-            pass
-        else:
+        if default is not None:
             node.setText(default)
         return default
-    
+
     def optional(self, content):
         if content.type.optional():
             return True
@@ -179,12 +175,12 @@ class Typed(Core):
             if a.optional():
                 return True
         return False
-    
+
     def encode(self, node, content):
-        # Add (soap) encoding information only if the resolved
-        # type is derived by extension.  Further, the xsi:type values
-        # is qualified by namespace only if the content (tag) and
-        # referenced type are in different namespaces.
+        # Add (soap) encoding information only if the resolved type is derived
+        # by extension. Further, the xsi:type values is qualified by namespace
+        # only if the content (tag) and referenced type are in different
+        # namespaces.
         if content.type.any():
             return
         if not content.real.extension():
@@ -196,12 +192,12 @@ class Typed(Core):
         if self.xstq:
             ns = content.real.namespace('ns1')
         Typer.manual(node, name, ns)
-    
+
     def skip(self, content):
         """
         Get whether to skip this I{content}.
-        Should be skipped when the content is optional
-        and either the value=None or the value is an empty list.
+        Should be skipped when the content is optional and either value=None or
+        the value is an empty list.
         @param content: The content to skip.
         @type content: L{Object}
         @return: True if content is to be skipped.
@@ -211,10 +207,10 @@ class Typed(Core):
             v = content.value
             if v is None:
                 return True
-            if isinstance(v, (list,tuple)) and len(v) == 0:
+            if isinstance(v, (list, tuple)) and len(v) == 0:
                 return True
         return False
-    
+
     def optional(self, content):
         if content.type.optional():
             return True
@@ -222,13 +218,13 @@ class Typed(Core):
             if a.optional():
                 return True
         return False
-    
+
     def translate(self, content):
         """
         Translate using the XSD type information.
-        Python I{dict} is translated to a suds object.  Most
-        importantly, primative values are translated from python
-        types to XML types using the XSD type.
+        Python I{dict} is translated to a suds object. Most importantly,
+        primative values are translated from python types to XML types using
+        the XSD type.
         @param content: The content to translate.
         @type content: L{Object}
         @return: self
@@ -246,11 +242,11 @@ class Typed(Core):
         v = content.real.translate(v, False)
         content.value = v
         return self
-        
+
     def sort(self, content):
         """
-        Sort suds object attributes based on ordering defined
-        in the XSD type information.
+        Sort suds object attributes based on ordering defined in the XSD type
+        information.
         @param content: The content to sort.
         @type content: L{Object}
         @return: self
@@ -264,8 +260,8 @@ class Typed(Core):
 
     def ordering(self, type):
         """
-        Get the attribute ordering defined in the specified
-        XSD type information.
+        Get the attribute ordering defined in the specified XSD type
+        information.
         @param type: An XSD type object.
         @type type: SchemaObject
         @return: An ordered list of attribute names.
@@ -285,7 +281,7 @@ class Typed(Core):
 class Literal(Typed):
     """
     A I{literal} marshaller.
-    This marshaller is semi-typed as needed to support both
-    I{document/literal} and I{rpc/literal} soap message styles.
+    This marshaller is semi-typed as needed to support both I{document/literal}
+    and I{rpc/literal} soap message styles.
     """
-    pass
\ No newline at end of file
+    pass
diff --git a/suds/mx/typer.py b/suds/mx/typer.py
index ea88df7..dc16fa5 100644
--- a/suds/mx/typer.py
+++ b/suds/mx/typer.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,14 +18,11 @@
 Provides sx typing classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.mx import *
 from suds.sax import Namespace as NS
 from suds.sax.text import Text
 
-log = getLogger(__name__)
-
 
 class Typer:
     """
@@ -43,7 +40,7 @@ class Typer:
         Text : ('string', NS.xsdns),
         bool : ('boolean', NS.xsdns),
      }
-                
+
     @classmethod
     def auto(cls, node, value=None):
         """
@@ -92,9 +89,9 @@ class Typer:
             ns = cls.genprefix(node, ns)
             qname = ':'.join((ns[0], tval))
             node.set(xta, qname)
-            node.addPrefix(ns[0], ns[1]) 
+            node.addPrefix(ns[0], ns[1])
         return node
-    
+
     @classmethod
     def genprefix(cls, node, ns):
         """
@@ -111,7 +108,7 @@ class Typer:
             if u is None or u == ns[1]:
                 return (p, ns[1])
         raise Exception('auto prefix, exhausted')
-    
+
     @classmethod
     def known(cls, object):
         try:
@@ -120,4 +117,3 @@ class Typer:
             return known
         except:
             pass
-
diff --git a/suds/options.py b/suds/options.py
index dabd770..2b2d489 100644
--- a/suds/options.py
+++ b/suds/options.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,11 +18,12 @@
 Suds basic options classes.
 """
 
+from suds.cache import Cache, NoCache
 from suds.properties import *
+from suds.store import DocumentStore, defaultDocumentStore
+from suds.transport import Transport
 from suds.wsse import Security
 from suds.xsd.doctor import Doctor
-from suds.transport import Transport
-from suds.cache import Cache, NoCache
 
 
 class TpLinker(AutoLinker):
@@ -30,7 +31,7 @@ class TpLinker(AutoLinker):
     Transport (auto) linker used to manage linkage between
     transport objects Properties and those Properties that contain them.
     """
-    
+
     def updated(self, properties, prev, next):
         if isinstance(prev, Transport):
             tp = Unskin(prev.options)
@@ -43,11 +44,22 @@ class TpLinker(AutoLinker):
 class Options(Skin):
     """
     Options:
-        - B{cache} - The XML document cache.  May be set (None) for no caching.
+        - B{cache} - The XML document cache. May be set to None for no caching.
                 - type: L{Cache}
-                - default: L{NoCache}
-        - B{faults} - Raise faults raised by server,
-            else return tuple from service method invocation as (httpcode, object).
+                - default: L{NoCache()}
+        - B{documentStore} - The XML document store used to access locally
+            stored documents without having to download them from an external
+            location. May be set to None for no internal suds library document
+            store.
+                - type: L{DocumentStore}
+                - default: L{defaultDocumentStore}
+        - B{extraArgumentErrors} - Raise exceptions when extra arguments are
+            detected when invoking a web service operation, compared to the
+            operation's WSDL schema definition.
+                - type: I{bool}
+                - default: True
+        - B{faults} - Raise faults raised by server, else return tuple from
+            service method invocation as (httpcode, object).
                 - type: I{bool}
                 - default: True
         - B{service} - The default service name.
@@ -56,7 +68,7 @@ class Options(Skin):
         - B{port} - The default service port name, not tcp port.
                 - type: I{str}
                 - default: None
-        - B{location} - This overrides the service port address I{URL} defined 
+        - B{location} - This overrides the service port address I{URL} defined
             in the WSDL.
                 - type: I{str}
                 - default: None
@@ -73,23 +85,24 @@ class Options(Skin):
                 - type: L{Doctor}
                 - default: None
         - B{xstq} - The B{x}ml B{s}chema B{t}ype B{q}ualified flag indicates
-            that the I{xsi:type} attribute values should be qualified by namespace.
+            that the I{xsi:type} attribute values should be qualified by
+            namespace.
                 - type: I{bool}
                 - default: True
-        - B{prefixes} - Elements of the soap message should be qualified (when needed)
-            using XML prefixes as opposed to xmlns="" syntax.
+        - B{prefixes} - Elements of the soap message should be qualified (when
+            needed) using XML prefixes as opposed to xmlns="" syntax.
                 - type: I{bool}
                 - default: True
-        - B{retxml} - Flag that causes the I{raw} soap envelope to be returned instead
-            of the python object graph.
+        - B{retxml} - Flag that causes the I{raw} soap envelope to be returned
+            instead of the python object graph.
                 - type: I{bool}
                 - default: False
-        - B{prettyxml} - Flag that causes I{pretty} xml to be rendered when generating
-            the outbound soap envelope.
+        - B{prettyxml} - Flag that causes I{pretty} xml to be rendered when
+            generating the outbound soap envelope.
                 - type: I{bool}
                 - default: False
-        - B{autoblend} - Flag that ensures that the schema(s) defined within the
-            WSDL import each other.
+        - B{autoblend} - Flag that ensures that the schema(s) defined within
+            the WSDL import each other.
                 - type: I{bool}
                 - default: False
         - B{cachingpolicy} - The caching policy.
@@ -99,16 +112,24 @@ class Options(Skin):
                 - default: 0
         - B{plugins} - A plugin container.
                 - type: I{list}
-        - B{nosend} - Create the soap envelope but don't send.
+                - default: I{list()}
+        - B{nosend} - Create the soap envelope but do not send.
             When specified, method invocation returns a I{RequestContext}
             instead of sending it.
                 - type: I{bool}
                 - default: False
-    """    
+        - B{unwrap} - Enable automatic parameter unwrapping when possible.
+            Enabled by default. If disabled, no input or output parameters are
+            ever automatically unwrapped.
+                - type: I{bool}
+                - default: True
+    """
     def __init__(self, **kwargs):
         domain = __name__
         definitions = [
             Definition('cache', Cache, NoCache()),
+            Definition('documentStore', DocumentStore, defaultDocumentStore),
+            Definition('extraArgumentErrors', bool, True),
             Definition('faults', bool, True),
             Definition('transport', Transport, None, TpLinker()),
             Definition('service', (int, basestring), None),
@@ -125,5 +146,5 @@ class Options(Skin):
             Definition('cachingpolicy', int, 0),
             Definition('plugins', (list, tuple), []),
             Definition('nosend', bool, False),
-        ]
+            Definition('unwrap', bool, True)]
         Skin.__init__(self, domain, definitions, kwargs)
diff --git a/suds/plugin.py b/suds/plugin.py
index 061c564..3579f5c 100644
--- a/suds/plugin.py
+++ b/suds/plugin.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -20,8 +20,8 @@ of suds plugins.
 """
 
 from suds import *
-from logging import getLogger
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -51,11 +51,11 @@ class DocumentContext(Context):
     """
     pass
 
-        
+
 class MessageContext(Context):
     """
-    The context for sending the soap envelope.
-    @ivar envelope: The soap envelope to be sent.
+    The context for sending the SOAP envelope.
+    @ivar envelope: The SOAP envelope to be sent.
     @type envelope: (str|L{sax.element.Element})
     @ivar reply: The reply.
     @type reply: (str|L{sax.element.Element}|object)
@@ -74,7 +74,7 @@ class InitPlugin(Plugin):
     """
     The base class for suds I{init} plugins.
     """
-    
+
     def initialized(self, context):
         """
         Suds client initialization.
@@ -90,17 +90,17 @@ class DocumentPlugin(Plugin):
     """
     The base class for suds I{document} plugins.
     """
-    
-    def loaded(self, context): 
+
+    def loaded(self, context):
         """
-        Suds has loaded a WSDL/XSD document.  Provides the plugin 
-        with an opportunity to inspect/modify the unparsed document. 
-        Called after each WSDL/XSD document is loaded. 
-        @param context: The document context. 
-        @type context: L{DocumentContext} 
+        Suds has loaded a WSDL/XSD document.  Provides the plugin
+        with an opportunity to inspect/modify the unparsed document.
+        Called after each WSDL/XSD document is loaded.
+        @param context: The document context.
+        @type context: L{DocumentContext}
         """
-        pass 
-    
+        pass
+
     def parsed(self, context):
         """
         Suds has parsed a WSDL/XSD document.  Provides the plugin
@@ -114,23 +114,23 @@ class DocumentPlugin(Plugin):
 
 class MessagePlugin(Plugin):
     """
-    The base class for suds I{soap message} plugins.
+    The base class for suds I{SOAP message} plugins.
     """
-    
+
     def marshalled(self, context):
         """
         Suds will send the specified soap envelope.
         Provides the plugin with the opportunity to inspect/modify
         the envelope Document before it is sent.
         @param context: The send context.
-            The I{envelope} is the envelope docuemnt.
+            The I{envelope} is the envelope document.
         @type context: L{MessageContext}
         """
         pass
-    
+
     def sending(self, context):
         """
-        Suds will send the specified soap envelope.
+        Suds will send the specified SOAP envelope.
         Provides the plugin with the opportunity to inspect/modify
         the message text it is sent.
         @param context: The send context.
@@ -138,7 +138,7 @@ class MessagePlugin(Plugin):
         @type context: L{MessageContext}
         """
         pass
-    
+
     def received(self, context):
         """
         Suds has received the specified reply.
@@ -149,7 +149,7 @@ class MessagePlugin(Plugin):
         @type context: L{MessageContext}
         """
         pass
-    
+
     def parsed(self, context):
         """
         Suds has sax parsed the received reply.
@@ -160,7 +160,7 @@ class MessagePlugin(Plugin):
         @type context: L{MessageContext}
         """
         pass
-    
+
     def unmarshalled(self, context):
         """
         Suds has unmarshalled the received reply.
@@ -172,7 +172,7 @@ class MessagePlugin(Plugin):
         """
         pass
 
-    
+
 class PluginContainer:
     """
     Plugin container provides easy method invocation.
@@ -181,20 +181,20 @@ class PluginContainer:
     @cvar ctxclass: A dict of plugin method / context classes.
     @type ctxclass: dict
     """
-    
+
     domains = {\
         'init': (InitContext, InitPlugin),
         'document': (DocumentContext, DocumentPlugin),
         'message': (MessageContext, MessagePlugin ),
     }
-    
+
     def __init__(self, plugins):
         """
         @param plugins: A list of plugin objects.
         @type plugins: [L{Plugin},]
         """
         self.plugins = plugins
-    
+
     def __getattr__(self, name):
         domain = self.domains.get(name)
         if domain:
@@ -206,8 +206,8 @@ class PluginContainer:
             return PluginDomain(ctx, plugins)
         else:
             raise Exception, 'plugin domain (%s), invalid' % name
-        
-        
+
+
 class PluginDomain:
     """
     The plugin domain.
@@ -216,11 +216,11 @@ class PluginDomain:
     @ivar plugins: A list of plugins (targets).
     @type plugins: list
     """
-    
+
     def __init__(self, ctx, plugins):
         self.ctx = ctx
         self.plugins = plugins
-    
+
     def __getattr__(self, name):
         return Method(name, self)
 
@@ -243,7 +243,7 @@ class Method:
         """
         self.name = name
         self.domain = domain
-            
+
     def __call__(self, **kwargs):
         ctx = self.domain.ctx()
         ctx.__dict__.update(kwargs)
diff --git a/suds/properties.py b/suds/properties.py
index 50b2593..5907d94 100644
--- a/suds/properties.py
+++ b/suds/properties.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,10 +18,6 @@
 Properties classes.
 """
 
-from logging import getLogger
-
-log = getLogger(__name__)
-
 
 class AutoLinker(object):
     """
@@ -58,7 +54,7 @@ class Link(object):
         self.validate(a, b)
         a.links.append(pB)
         b.links.append(pA)
-            
+
     def validate(self, pA, pB):
         """
         Validate that the two properties may be linked.
@@ -89,7 +85,7 @@ class Link(object):
             if k in kA:
                 raise Exception, 'Duplicate key %s found' % k
         return self
-            
+
     def teardown(self):
         """
         Teardown the link.
@@ -116,7 +112,7 @@ class Endpoint(object):
     def __init__(self, link, target):
         self.link = link
         self.target = target
-        
+
     def teardown(self):
         return self.link.teardown()
 
@@ -155,7 +151,7 @@ class Definition:
         self.classes = classes
         self.default = default
         self.linker = linker
-        
+
     def nvl(self, value=None):
         """
         Convert the I{value} into the default when I{None}.
@@ -168,7 +164,7 @@ class Definition:
             return self.default
         else:
             return value
-        
+
     def validate(self, value):
         """
         Validate the I{value} is of the correct class.
@@ -182,11 +178,11 @@ class Definition:
             not isinstance(value, self.classes):
                 msg = '"%s" must be: %s' % (self.name, self.classes)
                 raise AttributeError,msg
-                    
-            
+
+
     def __repr__(self):
         return '%s: %s' % (self.name, str(self))
-            
+
     def __str__(self):
         s = []
         if len(self.classes):
@@ -210,7 +206,7 @@ class Properties:
         a network of properties.
     @type links: [L{Property},..]
     @ivar defined: A dict of property values.
-    @type defined: dict 
+    @type defined: dict
     """
     def __init__(self, domain, definitions, kwargs):
         """
@@ -219,7 +215,7 @@ class Properties:
         @param definitions: A table of property definitions.
         @type definitions: {name: L{Definition}}
         @param kwargs: A list of property name/values to set.
-        @type kwargs: dict  
+        @type kwargs: dict
         """
         self.definitions = {}
         for d in definitions:
@@ -230,7 +226,7 @@ class Properties:
         self.modified = set()
         self.prime()
         self.update(kwargs)
-        
+
     def definition(self, name):
         """
         Get the definition for the property I{name}.
@@ -244,7 +240,7 @@ class Properties:
         if d is None:
             raise AttributeError(name)
         return d
-    
+
     def update(self, other):
         """
         Update the property values as specified by keyword/value.
@@ -258,7 +254,7 @@ class Properties:
         for n,v in other.items():
             self.set(n, v)
         return self
-    
+
     def notset(self, name):
         """
         Get whether a property has never been set by I{name}.
@@ -268,7 +264,7 @@ class Properties:
         @rtype: bool
         """
         self.provider(name).__notset(name)
-            
+
     def set(self, name, value):
         """
         Set the I{value} of a property by I{name}.
@@ -283,7 +279,7 @@ class Properties:
         """
         self.provider(name).__set(name, value)
         return self
-    
+
     def unset(self, name):
         """
         Unset a property by I{name}.
@@ -294,7 +290,7 @@ class Properties:
         """
         self.provider(name).__set(name, None)
         return self
-            
+
     def get(self, name, *df):
         """
         Get the value of a property by I{name}.
@@ -304,13 +300,13 @@ class Properties:
             is not set
         @type df: [1].
         @return: The stored value, or I{df[0]} if not set.
-        @rtype: any 
+        @rtype: any
         """
         return self.provider(name).__get(name, *df)
-    
+
     def link(self, other):
         """
-        Link (associate) this object with anI{other} properties object 
+        Link (associate) this object with anI{other} properties object
         to create a network of properties.  Links are bidirectional.
         @param other: The object to link.
         @type other: L{Properties}
@@ -334,7 +330,7 @@ class Properties:
             if p in others:
                 p.teardown()
         return self
-    
+
     def provider(self, name, history=None):
         """
         Find the provider of the property by I{name}.
@@ -362,7 +358,7 @@ class Properties:
         if len(history):
             return None
         return self
-    
+
     def keys(self, history=None):
         """
         Get the set of I{all} property names.
@@ -383,7 +379,7 @@ class Properties:
             keys.update(x.keys(history))
         history.remove(self)
         return keys
-    
+
     def domains(self, history=None):
         """
         Get the set of I{all} domain names.
@@ -404,7 +400,7 @@ class Properties:
             domains.update(x.domains(history))
         history.remove(self)
         return domains
- 
+
     def prime(self):
         """
         Prime the stored values based on default values
@@ -415,10 +411,10 @@ class Properties:
         for d in self.definitions.values():
             self.defined[d.name] = d.default
         return self
-    
+
     def __notset(self, name):
         return not (name in self.modified)
-    
+
     def __set(self, name, value):
         d = self.definition(name)
         d.validate(value)
@@ -427,14 +423,14 @@ class Properties:
         self.defined[name] = value
         self.modified.add(name)
         d.linker.updated(self, prev, value)
-        
+
     def __get(self, name, *df):
         d = self.definition(name)
         value = self.defined.get(name)
         if value == d.default and len(df):
             value = df[0]
         return value
-            
+
     def str(self, history):
         s = []
         s.append('Definitions:')
@@ -450,10 +446,10 @@ class Properties:
                 s.append(x.str(history))
             history.remove(self)
         return '\n'.join(s)
-            
+
     def __repr__(self):
         return str(self)
-            
+
     def __str__(self):
         return self.str([])
 
@@ -466,36 +462,36 @@ class Skin(object):
     """
     def __init__(self, domain, definitions, kwargs):
         self.__pts__ = Properties(domain, definitions, kwargs)
-        
+
     def __setattr__(self, name, value):
         builtin = name.startswith('__') and name.endswith('__')
         if builtin:
             self.__dict__[name] = value
             return
         self.__pts__.set(name, value)
-        
+
     def __getattr__(self, name):
         return self.__pts__.get(name)
-    
+
     def __repr__(self):
         return str(self)
-    
+
     def __str__(self):
         return str(self.__pts__)
-    
-    
+
+
 class Unskin(object):
     def __new__(self, *args, **kwargs):
         return args[0].__pts__
-    
-    
+
+
 class Inspector:
     """
     Wrapper inspector.
     """
     def __init__(self, options):
         self.properties = options.__pts__
-        
+
     def get(self, name, *df):
         """
         Get the value of a property by I{name}.
@@ -505,7 +501,7 @@ class Inspector:
             is not set
         @type df: [1].
         @return: The stored value, or I{df[0]} if not set.
-        @rtype: any 
+        @rtype: any
         """
         return self.properties.get(name, *df)
 
@@ -521,7 +517,7 @@ class Inspector:
 
     def link(self, other):
         """
-        Link (associate) this object with anI{other} properties object 
+        Link (associate) this object with anI{other} properties object
         to create a network of properties.  Links are bidirectional.
         @param other: The object to link.
         @type other: L{Properties}
@@ -530,7 +526,7 @@ class Inspector:
         """
         p = other.__pts__
         return self.properties.link(p)
-    
+
     def unlink(self, other):
         """
         Unlink (disassociate) the specified properties object.
diff --git a/suds/reader.py b/suds/reader.py
index 1184f12..ad71642 100644
--- a/suds/reader.py
+++ b/suds/reader.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,24 +15,21 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains xml document reader classes.
+  XML document reader classes providing integration with the suds library's
+caching system.
 """
 
 
-from suds.sax.parser import Parser
-from suds.transport import Request
 from suds.cache import Cache, NoCache
-from suds.store import DocumentStore
 from suds.plugin import PluginContainer
-from logging import getLogger
-
-
-log = getLogger(__name__)
+from suds.sax.parser import Parser
+from suds.store import DocumentStore
+from suds.transport import Request
 
 
 class Reader:
     """
-    The reader provides integration with cache.
+    Provides integration with the cache.
     @ivar options: An options object.
     @type options: I{Options}
     """
@@ -56,18 +53,16 @@ class Reader:
 
 class DocumentReader(Reader):
     """
-    The XML document reader provides an integration
-    between the SAX L{Parser} and the document cache.
+    Provides integration between the SAX L{Parser} and the document cache.
     """
-    
+
     def open(self, url):
         """
-        Open an XML document at the specified I{url}.
-        First, the document attempted to be retrieved from
-        the I{object cache}.  If not found, it is downloaded and
-        parsed using the SAX parser.  The result is added to the
-        cache for the next open().
-        @param url: A document url.
+        Open an XML document at the specified I{URL}.
+        First, the document attempted to be retrieved from the I{object cache}.
+        If not found, it is downloaded and parsed using the SAX parser. The
+        result is added to the cache for the next open().
+        @param url: A document URL.
         @type url: str.
         @return: The specified XML document.
         @rtype: I{Document}
@@ -80,67 +75,70 @@ class DocumentReader(Reader):
             cache.put(id, d)
         self.plugins.document.parsed(url=url, document=d.root())
         return d
-    
+
     def download(self, url):
         """
-        Download the docuemnt.
-        @param url: A document url.
+        Download the document.
+        @param url: A document URL.
         @type url: str.
-        @return: A file pointer to the docuemnt.
+        @return: A file pointer to the document.
         @rtype: file-like
         """
-        store = DocumentStore()
-        fp = store.open(url)
-        if fp is None:
+        content = None
+        store = self.options.documentStore
+        if store is not None:
+            content = store.open(url)
+        if content is None:
             fp = self.options.transport.open(Request(url))
-        content = fp.read()
-        fp.close()
+            try:
+                content = fp.read()
+            finally:
+                fp.close()
         ctx = self.plugins.document.loaded(url=url, document=content)
-        content = ctx.document 
+        content = ctx.document
         sax = Parser()
         return sax.parse(string=content)
-    
+
     def cache(self):
         """
         Get the cache.
-        @return: The I{options} when I{cachingpolicy} = B{0}.
+        @return: The I{cache} when I{cachingpolicy} = B{0}.
         @rtype: L{Cache}
         """
         if self.options.cachingpolicy == 0:
             return self.options.cache
-        else:
-            return NoCache()
+        return NoCache()
 
 
 class DefinitionsReader(Reader):
     """
-    The WSDL definitions reader provides an integration
-    between the Definitions and the object cache.
+    Provides integration between the WSDL Definitions object and the object
+    cache.
     @ivar fn: A factory function (constructor) used to
         create the object not found in the cache.
     @type fn: I{Constructor}
     """
-    
+
     def __init__(self, options, fn):
         """
         @param options: An options object.
         @type options: I{Options}
-        @param fn: A factory function (constructor) used to
-            create the object not found in the cache.
+        @param fn: A factory function (constructor) used to create the object
+            not found in the cache.
         @type fn: I{Constructor}
         """
         Reader.__init__(self, options)
         self.fn = fn
-    
+
     def open(self, url):
         """
-        Open a WSDL at the specified I{url}.
+        Open a WSDL at the specified I{URL}.
         First, the WSDL attempted to be retrieved from
         the I{object cache}.  After unpickled from the cache, the
         I{options} attribute is restored.
-        If not found, it is downloaded and instantiated using the 
+        If not found, it is downloaded and instantiated using the
         I{fn} constructor and added to the cache for the next open().
-        @param url: A WSDL url.
+        @param url: A WSDL URL.
         @type url: str.
         @return: The WSDL object.
         @rtype: I{Definitions}
@@ -160,10 +158,9 @@ class DefinitionsReader(Reader):
     def cache(self):
         """
         Get the cache.
-        @return: The I{options} when I{cachingpolicy} = B{1}.
+        @return: The I{cache} when I{cachingpolicy} = B{1}.
         @rtype: L{Cache}
         """
         if self.options.cachingpolicy == 1:
             return self.options.cache
-        else:
-            return NoCache()
\ No newline at end of file
+        return NoCache()
diff --git a/suds/resolver.py b/suds/resolver.py
index 278b5da..2a30169 100644
--- a/suds/resolver.py
+++ b/suds/resolver.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -19,13 +19,14 @@ The I{resolver} module provides a collection of classes that
 provide wsdl/xsd named type resolution.
 """
 
-import re
-from logging import getLogger
 from suds import *
 from suds.sax import splitPrefix, Namespace
 from suds.sudsobject import Object
 from suds.xsd.query import BlindQuery, TypeQuery, qualify
 
+import re
+
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -42,7 +43,7 @@ class Resolver:
         @type schema: L{xsd.schema.Schema}
         """
         self.schema = schema
-        
+
     def find(self, name, resolved=True):
         """
         Get the definition object for the schema object by name.
@@ -69,7 +70,7 @@ class Resolver:
 
 class PathResolver(Resolver):
     """
-    Resolveds the definition object for the schema type located at the specified path.
+    Resolves the definition object for the schema type located at a given path.
     The path may contain (.) dot notation to specify nested types.
     @ivar wsdl: A wsdl object.
     @type wsdl: L{wsdl.Definitions}
@@ -114,7 +115,7 @@ class PathResolver(Resolver):
         except PathResolver.BadPath:
             log.error('path: "%s", not-found' % path)
         return result
-    
+
     def root(self, parts):
         """
         Find the path root.
@@ -132,13 +133,12 @@ class PathResolver(Resolver):
         if result is None:
             log.error('(%s) not-found', name)
             raise PathResolver.BadPath(name)
-        else:
-            log.debug('found (%s) as (%s)', name, Repr(result))
+        log.debug('found (%s) as (%s)', name, Repr(result))
         return result
-    
+
     def branch(self, root, parts):
         """
-        Traverse the path until the leaf is reached.
+        Traverse the path until a leaf is reached.
         @param parts: A list of path parts.
         @type parts: [str,..]
         @param root: The root.
@@ -154,11 +154,10 @@ class PathResolver(Resolver):
             if result is None:
                 log.error('(%s) not-found', name)
                 raise PathResolver.BadPath(name)
-            else:
-                result = result.resolve(nobuiltin=True)
-                log.debug('found (%s) as (%s)', name, Repr(result))
+            result = result.resolve(nobuiltin=True)
+            log.debug('found (%s) as (%s)', name, Repr(result))
         return result
-    
+
     def leaf(self, parent, parts):
         """
         Find the leaf.
@@ -177,7 +176,7 @@ class PathResolver(Resolver):
         if result is None:
             raise PathResolver.BadPath(name)
         return result
-    
+
     def qualify(self, name):
         """
         Qualify the name as either:
@@ -186,7 +185,7 @@ class PathResolver(Resolver):
           - fully ns qualified name (eg: {http://myns-uri}Person)
         @param name: The name of an object in the schema.
         @type name: str
-        @return: A qualifed name.
+        @return: A qualified name.
         @rtype: qname
         """
         m = self.altp.match(name)
@@ -194,12 +193,12 @@ class PathResolver(Resolver):
             return qualify(name, self.wsdl.root, self.wsdl.tns)
         else:
             return (m.group(4), m.group(2))
-        
+
     def split(self, s):
         """
         Split the string on (.) while preserving any (.) inside the
         '{}' alternalte syntax for full ns qualification.
-        @param s: A plain or qualifed name.
+        @param s: A plain or qualified name.
         @type s: str
         @return: A list of the name's parts.
         @rtype: [str,..]
@@ -214,7 +213,7 @@ class PathResolver(Resolver):
             parts.append(s[b:e])
             b = e+1
         return parts
-    
+
     class BadPath(Exception): pass
 
 
@@ -227,7 +226,7 @@ class TreeResolver(Resolver):
     @ivar stack: The context stack.
     @type stack: list
     """
-    
+
     def __init__(self, schema):
         """
         @param schema: A schema object.
@@ -235,13 +234,13 @@ class TreeResolver(Resolver):
         """
         Resolver.__init__(self, schema)
         self.stack = Stack()
-        
+
     def reset(self):
         """
         Reset the resolver's state.
         """
         self.stack = Stack()
-            
+
     def push(self, x):
         """
         Push an I{object} onto the stack.
@@ -257,7 +256,7 @@ class TreeResolver(Resolver):
         self.stack.append(frame)
         log.debug('push: (%s)\n%s', Repr(frame), Repr(self.stack))
         return frame
-    
+
     def top(self):
         """
         Get the I{frame} at the top of the stack.
@@ -268,21 +267,20 @@ class TreeResolver(Resolver):
             return self.stack[-1]
         else:
             return Frame.Empty()
-        
+
     def pop(self):
         """
         Pop the frame at the top of the stack.
         @return: The popped frame, else None.
         @rtype: L{Frame}
         """
-        if len(self.stack):      
+        if len(self.stack):
             popped = self.stack.pop()
             log.debug('pop: (%s)\n%s', Repr(popped), Repr(self.stack))
             return popped
-        else:
-            log.debug('stack empty, not-popped')
+        log.debug('stack empty, not-popped')
         return None
-    
+
     def depth(self):
         """
         Get the current stack depth.
@@ -290,14 +288,13 @@ class TreeResolver(Resolver):
         @rtype: int
         """
         return len(self.stack)
-    
+
     def getchild(self, name, parent):
-        """ get a child by name """
+        """Get a child by name."""
         log.debug('searching parent (%s) for (%s)', Repr(parent), name)
         if name.startswith('@'):
             return parent.get_attribute(name[1:])
-        else:
-            return parent.get_child(name)
+        return parent.get_child(name)
 
 
 class NodeResolver(TreeResolver):
@@ -307,14 +304,14 @@ class NodeResolver(TreeResolver):
     the tree structure to ensure that nodes are resolved in
     context.
     """
-    
+
     def __init__(self, schema):
         """
         @param schema: A schema object.
         @type schema: L{xsd.schema.Schema}
         """
         TreeResolver.__init__(self, schema)
-        
+
     def find(self, node, resolved=False, push=True):
         """
         @param node: An xml node to be resolved.
@@ -343,7 +340,7 @@ class NodeResolver(TreeResolver):
         if resolved:
             result = result.resolve()
         return result
-    
+
     def findattr(self, name, resolved=True):
         """
         Find an attribute type definition.
@@ -366,24 +363,24 @@ class NodeResolver(TreeResolver):
         if resolved:
             result = result.resolve()
         return result
-    
+
     def query(self, name, node):
-        """ blindly query the schema by name """
+        """Blindly query the schema by name."""
         log.debug('searching schema for (%s)', name)
         qref = qualify(name, node, node.namespace())
         query = BlindQuery(qref)
         result = query.execute(self.schema)
         return (result, [])
-    
+
     def known(self, node):
-        """ resolve type referenced by @xsi:type """
+        """Resolve type referenced by @xsi:type."""
         ref = node.get('type', Namespace.xsins)
         if ref is None:
             return None
         qref = qualify(ref, node, node.namespace())
         query = BlindQuery(qref)
         return query.execute(self.schema)
-        
+
 
 class GraphResolver(TreeResolver):
     """
@@ -392,20 +389,20 @@ class GraphResolver(TreeResolver):
     the tree structure to ensure that nodes are resolved in
     context.
     """
-    
+
     def __init__(self, schema):
         """
         @param schema: A schema object.
         @type schema: L{xsd.schema.Schema}
         """
         TreeResolver.__init__(self, schema)
-        
+
     def find(self, name, object, resolved=False, push=True):
         """
         @param name: The name of the object to be resolved.
         @type name: basestring
         @param object: The name's value.
-        @type object: (any|L{Object}) 
+        @type object: (any|L{Object})
         @param resolved: A flag indicating that the fully resolved type
             should be returned.
         @type resolved: boolean
@@ -434,9 +431,9 @@ class GraphResolver(TreeResolver):
             else:
                 result = known
         return result
-    
+
     def query(self, name):
-        """ blindly query the schema by name """
+        """Blindly query the schema by name."""
         log.debug('searching schema for (%s)', name)
         schema = self.schema
         wsdl = self.wsdl()
@@ -447,17 +444,17 @@ class GraphResolver(TreeResolver):
         query = BlindQuery(qref)
         result = query.execute(schema)
         return (result, [])
-    
+
     def wsdl(self):
-        """ get the wsdl """
+        """Get the wsdl."""
         container = self.schema.container
         if container is None:
             return None
         else:
             return container.wsdl
-    
+
     def known(self, object):
-        """ get the type specified in the object's metadata """
+        """Get the type specified in the object's metadata."""
         try:
             md = object.__metadata__
             known = md.sxtype
@@ -465,7 +462,7 @@ class GraphResolver(TreeResolver):
         except:
             pass
 
-       
+
 class Frame:
     def __init__(self, type, resolved=None, ancestry=()):
         self.type = type
@@ -479,7 +476,7 @@ class Frame:
             (Repr(self.type),
             Repr(self.resolved),
             [Repr(t) for t in self.ancestry])
-            
+
     class Empty:
         def __getattr__(self, name):
             if name == 'ancestry':
@@ -493,4 +490,4 @@ class Stack(list):
         result = []
         for item in self:
             result.append(repr(item))
-        return '\n'.join(result)
\ No newline at end of file
+        return '\n'.join(result)
diff --git a/suds/sax/__init__.py b/suds/sax/__init__.py
index 3d71432..be13823 100644
--- a/suds/sax/__init__.py
+++ b/suds/sax/__init__.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,17 +15,16 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The sax module contains a collection of classes that provide a
-(D)ocument (O)bject (M)odel representation of an XML document.
-The goal is to provide an easy, intuative interface for managing XML
-documents.  Although, the term, DOM, is used above, this model is
-B{far} better.
+The sax module contains a collection of classes that provide a (D)ocument
+(O)bject (M)odel representation of an XML document. The goal is to provide an
+easy, intuitive interface for managing XML documents. Although, the term, DOM,
+is used above, this model is B{far} better.
 
-XML namespaces in suds are represented using a (2) element tuple
-containing the prefix and the URI.  Eg: I{('tns', 'http://myns')}
+XML namespaces in suds are represented using a (2) element tuple containing the
+prefix and the URI, e.g. I{('tns', 'http://myns')}
 
- at var encoder: A I{pluggable} XML special character processor used to
-    encode/decode strings.
+ at var encoder: A I{pluggable} XML special character processor used to encode/
+    decode strings.
 @type encoder: L{Encoder}
 """
 
@@ -39,20 +38,18 @@ encoder = Encoder()
 
 def splitPrefix(name):
     """
-    Split the name into a tuple (I{prefix}, I{name}).  The first element in
-    the tuple is I{None} when the name does't have a prefix.
+    Split the name into a tuple (I{prefix}, I{name}). The first element in the
+    tuple is I{None} when the name does not have a prefix.
     @param name: A node name containing an optional prefix.
     @type name: basestring
     @return: A tuple containing the (2) parts of I{name}
-    @rtype: (I{prefix}, I{name}) 
+    @rtype: (I{prefix}, I{name})
     """
-    if isinstance(name, basestring) \
-        and ':' in name:
-            return tuple(name.split(':', 1))
-    else:
-        return (None, name)
+    if isinstance(name, basestring) and ':' in name:
+        return tuple(name.split(':', 1))
+    return None, name
+
 
-   
 class Namespace:
     """
     The namespace class represents XML namespaces.
@@ -63,15 +60,15 @@ class Namespace:
     xsdns = ('xs', 'http://www.w3.org/2001/XMLSchema')
     xsins = ('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
     all = (xsdns, xsins)
-    
+
     @classmethod
     def create(cls, p=None, u=None):
-        return (p, u)
-    
+        return p, u
+
     @classmethod
     def none(cls, ns):
-        return ( ns == cls.default )
-    
+        return ns == cls.default
+
     @classmethod
     def xsd(cls, ns):
         try:
@@ -79,7 +76,7 @@ class Namespace:
         except:
             pass
         return False
-    
+
     @classmethod
     def xsi(cls, ns):
         try:
@@ -87,10 +84,10 @@ class Namespace:
         except:
             pass
         return False
-    
+
     @classmethod
     def xs(cls, ns):
-        return ( cls.xsd(ns) or cls.xsi(ns) )
+        return cls.xsd(ns) or cls.xsi(ns)
 
     @classmethod
     def w3(cls, ns):
@@ -99,7 +96,7 @@ class Namespace:
         except:
             pass
         return False
-    
+
     @classmethod
     def isns(cls, ns):
         try:
diff --git a/suds/sax/attribute.py b/suds/sax/attribute.py
index 86dfb11..61c5ad7 100644
--- a/suds/sax/attribute.py
+++ b/suds/sax/attribute.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,15 +18,12 @@
 Provides XML I{attribute} classes.
 """
 
-import suds.sax
-from logging import getLogger
 from suds import *
 from suds.sax import *
 from suds.sax.text import Text
 
-log = getLogger(__name__)
 
-class Attribute:
+class Attribute(UnicodeMixin):
     """
     An XML attribute object.
     @ivar parent: The node containing this attribute
@@ -43,12 +40,12 @@ class Attribute:
         @param name: The attribute's name with I{optional} namespace prefix.
         @type name: basestring
         @param value: The attribute's value
-        @type value: basestring 
+        @type value: basestring
         """
         self.parent = None
         self.prefix, self.name = splitPrefix(name)
         self.setValue(value)
-        
+
     def clone(self, parent=None):
         """
         Clone this object.
@@ -60,7 +57,7 @@ class Attribute:
         a = Attribute(self.qname(), self.value)
         a.parent = parent
         return a
-    
+
     def qname(self):
         """
         Get the B{fully} qualified name of this attribute
@@ -71,7 +68,7 @@ class Attribute:
             return self.name
         else:
             return ':'.join((self.prefix, self.name))
-        
+
     def setValue(self, value):
         """
         Set the attributes value
@@ -85,7 +82,7 @@ class Attribute:
         else:
             self.value = Text(value)
         return self
-        
+
     def getValue(self, default=Text('')):
         """
         Get the attributes value with optional default.
@@ -99,7 +96,7 @@ class Attribute:
             return self.value
         else:
             return default
-    
+
     def hasText(self):
         """
         Get whether the attribute has I{text} and that it is not an empty
@@ -108,7 +105,7 @@ class Attribute:
         @rtype: boolean
         """
         return ( self.value is not None and len(self.value) )
-        
+
     def namespace(self):
         """
         Get the attributes namespace.  This may either be the namespace
@@ -120,7 +117,7 @@ class Attribute:
             return Namespace.default
         else:
             return self.resolvePrefix(self.prefix)
-        
+
     def resolvePrefix(self, prefix):
         """
         Resolve the specified prefix to a known namespace.
@@ -133,7 +130,7 @@ class Attribute:
         if self.parent is not None:
             ns = self.parent.resolvePrefix(prefix)
         return ns
-    
+
     def match(self, name=None, ns=None):
         """
         Match by (optional) name and/or (optional) namespace.
@@ -153,24 +150,20 @@ class Attribute:
         else:
             byns = ( self.namespace()[1] == ns[1] )
         return ( byname and byns )
-    
+
     def __eq__(self, rhs):
         """ equals operator """
         return rhs is not None and \
             isinstance(rhs, Attribute) and \
             self.prefix == rhs.name and \
             self.name == rhs.name
-            
+
     def __repr__(self):
         """ get a string representation """
         return \
             'attr (prefix=%s, name=%s, value=(%s))' %\
                 (self.prefix, self.name, self.value)
 
-    def __str__(self):
-        """ get an xml string representation """
-        return unicode(self).encode('utf-8')
-    
     def __unicode__(self):
         """ get an xml string representation """
         n = self.qname()
diff --git a/suds/sax/date.py b/suds/sax/date.py
index 6e31c4c..c5e5e93 100644
--- a/suds/sax/date.py
+++ b/suds/sax/date.py
@@ -1,378 +1,458 @@
-# This program is free software; you can redistribute it and/or modify
+# This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU Library Lesser General Public License for more details at
 # ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Nathan Van Gheem (vangheem at gmail.com)
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+# based on code by: Glen Walker
+# based on code by: Nathan Van Gheem ( vangheem at gmail.com )
 
-"""
-The I{xdate} module provides classes for converstion
-between XML dates and python objects.
-"""
+"""Classes for conversion between XML dates and Python objects."""
 
-from logging import getLogger
-from suds import *
-from suds.xsd import *
-import time
-import datetime as dt
+from suds import UnicodeMixin
+
+import datetime
 import re
+import time
+
+
+_SNIPPET_DATE =  \
+    r"(?P<year>\d{1,})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
+_SNIPPET_TIME =  \
+    r"(?P<hour>\d{1,2}):(?P<minute>[0-5]?[0-9]):(?P<second>[0-5]?[0-9])"  \
+    r"(?:\.(?P<subsecond>\d+))?"
+_SNIPPET_ZONE =  \
+    r"(?:(?P<tz_sign>[-+])(?P<tz_hour>\d{1,2})"  \
+    r"(?::(?P<tz_minute>[0-5]?[0-9]))?)"  \
+    r"|(?P<tz_utc>[Zz])"
 
-log = getLogger(__name__)
+_PATTERN_DATE = r"^%s(?:%s)?$" % (_SNIPPET_DATE, _SNIPPET_ZONE)
+_PATTERN_TIME = r"^%s(?:%s)?$" % (_SNIPPET_TIME, _SNIPPET_ZONE)
+_PATTERN_DATETIME = r"^%s[T ]%s(?:%s)?$" % (_SNIPPET_DATE, _SNIPPET_TIME,
+                                            _SNIPPET_ZONE)
 
+_RE_DATE = re.compile(_PATTERN_DATE)
+_RE_TIME = re.compile(_PATTERN_TIME)
+_RE_DATETIME = re.compile(_PATTERN_DATETIME)
 
-class Date:
+
+class Date(UnicodeMixin):
     """
-    An XML date object.
-    Supported formats:
-        - YYYY-MM-DD
-        - YYYY-MM-DD(z|Z)
-        - YYYY-MM-DD+06:00
-        - YYYY-MM-DD-06:00
-    @ivar date: The object value.
-    @type date: B{datetime}.I{date}
+    An XML date object supporting the xsd:date datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{date}
+
     """
-    def __init__(self, date):
-        """
-        @param date: The value of the object.
-        @type date: (date|str)
-        @raise ValueError: When I{date} is invalid.
-        """
-        if isinstance(date, dt.date):
-            self.date = date
-            return
-        if isinstance(date, basestring):
-            self.date = self.__parse(date)
-            return
-        raise ValueError, type(date)
-    
-    def year(self):
-        """
-        Get the I{year} component.
-        @return: The year.
-        @rtype: int
-        """
-        return self.date.year
-    
-    def month(self):
-        """
-        Get the I{month} component.
-        @return: The month.
-        @rtype: int
-        """
-        return self.date.month
-    
-    def day(self):
+
+    def __init__(self, value):
         """
-        Get the I{day} component.
-        @return: The day.
-        @rtype: int
+        @param value: The date value of the object.
+        @type value: (datetime.date|str)
+        @raise ValueError: When I{value} is invalid.
+
         """
-        return self.date.day
-        
-    def __parse(self, s):
+        if isinstance(value, datetime.datetime):
+            self.value = value.date()
+        elif isinstance(value, datetime.date):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for Date(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
         """
         Parse the string date.
-        Supported formats:
-            - YYYY-MM-DD
-            - YYYY-MM-DD(z|Z)
-            - YYYY-MM-DD+06:00
-            - YYYY-MM-DD-06:00
-        Although, the TZ is ignored because it's meaningless
-        without the time, right?
-        @param s: A date string.
-        @type s: str
+
+        Supports the subset of ISO8601 used by xsd:date, but is lenient with
+        what is accepted, handling most reasonable syntax.
+
+        Any timezone is parsed but ignored because a) it is meaningless without
+        a time and b) B{datetime}.I{date} does not support timezone
+        information.
+
+        @param value: A date string.
+        @type value: str
         @return: A date object.
-        @rtype: I{date}
+        @rtype: B{datetime}.I{date}
+
         """
-        try:
-            year, month, day = s[:10].split('-', 2)
-            year = int(year)
-            month = int(month)
-            day = int(day)
-            return dt.date(year, month, day)
-        except:
-            log.debug(s, exec_info=True)
-            raise ValueError, 'Invalid format "%s"' % s
-        
-    def __str__(self):
-        return unicode(self)
-    
+        match_result = _RE_DATE.match(value)
+        if match_result is None:
+            raise ValueError("date data has invalid format '%s'" % (value,))
+        return _date_from_match(match_result)
+
     def __unicode__(self):
-        return self.date.isoformat()
+        return self.value.isoformat()
 
 
-class Time:
+class DateTime(UnicodeMixin):
     """
-    An XML time object.
-    Supported formats:
-        - HH:MI:SS
-        - HH:MI:SS(z|Z)
-        - HH:MI:SS.ms
-        - HH:MI:SS.ms(z|Z)
-        - HH:MI:SS(+|-)06:00
-        - HH:MI:SS.ms(+|-)06:00
-    @ivar tz: The timezone
-    @type tz: L{Timezone}
-    @ivar date: The object value.
-    @type date: B{datetime}.I{time}
+    An XML datetime object supporting the xsd:dateTime datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{datetime}
+
     """
-    
-    def __init__(self, time, adjusted=True):
+
+    def __init__(self, value):
         """
-        @param time: The value of the object.
-        @type time: (time|str)
-        @param adjusted: Adjust for I{local} Timezone.
-        @type adjusted: boolean
-        @raise ValueError: When I{time} is invalid.
+        @param value: The datetime value of the object.
+        @type value: (datetime.datetime|str)
+        @raise ValueError: When I{value} is invalid.
+
         """
-        self.tz = Timezone()
-        if isinstance(time, dt.time):
-            self.time = time
-            return
-        if isinstance(time, basestring):
-            self.time = self.__parse(time)
-            if adjusted:
-                self.__adjust()
-            return
-        raise ValueError, type(time)
-    
-    def hour(self):
+        if isinstance(value, datetime.datetime):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for DateTime(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
         """
-        Get the I{hour} component.
-        @return: The hour.
-        @rtype: int
+        Parse the string datetime.
+
+        Supports the subset of ISO8601 used by xsd:dateTime, but is lenient
+        with what is accepted, handling most reasonable syntax.
+
+        Subsecond information is rounded to microseconds due to a restriction
+        in the python datetime.datetime/time implementation.
+
+        @param value: A datetime string.
+        @type value: str
+        @return: A datetime object.
+        @rtype: B{datetime}.I{datetime}
+
         """
-        return self.time.hour
-    
-    def minute(self):
+        match_result = _RE_DATETIME.match(value)
+        if match_result is None:
+           raise ValueError("date data has invalid format '%s'" % (value,))
+
+        date = _date_from_match(match_result)
+        time, round_up = _time_from_match(match_result)
+        tzinfo = _tzinfo_from_match(match_result)
+
+        value = datetime.datetime.combine(date, time)
+        value = value.replace(tzinfo=tzinfo)
+        if round_up:
+            value += datetime.timedelta(microseconds=1)
+        return value
+
+    def __unicode__(self):
+        return self.value.isoformat()
+
+
+class Time(UnicodeMixin):
+    """
+    An XML time object supporting the xsd:time datatype.
+
+    @ivar value: The object value.
+    @type value: B{datetime}.I{time}
+
+    """
+
+    def __init__(self, value):
         """
-        Get the I{minute} component.
-        @return: The minute.
-        @rtype: int
+        @param value: The time value of the object.
+        @type value: (datetime.time|str)
+        @raise ValueError: When I{value} is invalid.
+
         """
-        return self.time.minute
-    
-    def second(self):
+        if isinstance(value, datetime.time):
+            self.value = value
+        elif isinstance(value, basestring):
+            self.value = self.__parse(value)
+        else:
+            raise ValueError("invalid type for Time(): %s" % type(value))
+
+    @staticmethod
+    def __parse(value):
         """
-        Get the I{seconds} component.
-        @return: The seconds.
-        @rtype: int
+        Parse the string date.
+
+        Supports the subset of ISO8601 used by xsd:time, but is lenient with
+        what is accepted, handling most reasonable syntax.
+
+        Subsecond information is rounded to microseconds due to a restriction
+        in the python datetime.time implementation.
+
+        @param value: A time string.
+        @type value: str
+        @return: A time object.
+        @rtype: B{datetime}.I{time}
+
         """
-        return self.time.second
-    
-    def microsecond(self):
+        match_result = _RE_TIME.match(value)
+        if match_result is None:
+           raise ValueError("date data has invalid format '%s'" % (value,))
+
+        time, round_up = _time_from_match(match_result)
+        tzinfo = _tzinfo_from_match(match_result)
+        if round_up:
+            time = _bump_up_time_by_microsecond(time)
+        return time.replace(tzinfo=tzinfo)
+
+    def __unicode__(self):
+        return self.value.isoformat()
+
+
+class FixedOffsetTimezone(datetime.tzinfo, UnicodeMixin):
+    """
+    A timezone with a fixed offset and no daylight savings adjustment.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
+    """
+
+    def __init__(self, offset):
         """
-        Get the I{microsecond} component.
-        @return: The microsecond.
-        @rtype: int
+        @param offset: The fixed offset of the timezone.
+        @type offset: I{int} or B{datetime}.I{timedelta}
+
         """
-        return self.time.microsecond
-    
-    def __adjust(self):
+        if type(offset) == int:
+            offset = datetime.timedelta(hours=offset)
+        elif type(offset) != datetime.timedelta:
+            raise TypeError("timezone offset must be an int or "
+                "datetime.timedelta")
+        if offset.microseconds or (offset.seconds % 60 != 0):
+            raise ValueError("timezone offset must have minute precision")
+        self.__offset = offset
+
+    def dst(self, dt):
         """
-        Adjust for TZ offset.
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
+
         """
-        if hasattr(self, 'offset'):
-            today = dt.date.today()
-            delta = self.tz.adjustment(self.offset)
-            d = dt.datetime.combine(today, self.time)
-            d = ( d + delta )
-            self.time = d.time()
-        
-    def __parse(self, s):
+        return datetime.timedelta(0)
+
+    def utcoffset(self, dt):
         """
-        Parse the string date.
-        Patterns:
-            - HH:MI:SS
-            - HH:MI:SS(z|Z)
-            - HH:MI:SS.ms
-            - HH:MI:SS.ms(z|Z)
-            - HH:MI:SS(+|-)06:00
-            - HH:MI:SS.ms(+|-)06:00
-        @param s: A time string.
-        @type s: str
-        @return: A time object.
-        @rtype: B{datetime}.I{time}
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
+
         """
-        try:
-            offset = None
-            part = Timezone.split(s)
-            hour, minute, second = part[0].split(':', 2)
-            hour = int(hour)
-            minute = int(minute)
-            second, ms = self.__second(second)
-            if len(part) == 2:
-                self.offset = self.__offset(part[1])
-            if ms is None:
-                return dt.time(hour, minute, second)
-            else:
-                return dt.time(hour, minute, second, ms)
-        except:
-            log.debug(s, exec_info=True)
-            raise ValueError, 'Invalid format "%s"' % s
-        
-    def __second(self, s):
+        return self.__offset
+
+    def tzname(self, dt):
         """
-        Parse the seconds and microseconds.
-        The microseconds are truncated to 999999 due to a restriction in
-        the python datetime.datetime object.
-        @param s: A string representation of the seconds.
-        @type s: str
-        @return: Tuple of (sec,ms)
-        @rtype: tuple.
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
         """
-        part = s.split('.')
-        if len(part) > 1:
-            return (int(part[0]), int(part[1][:6]))
+        # total_seconds was introduced in Python 2.7
+        if hasattr(self.__offset, "total_seconds"):
+            total_seconds = self.__offset.total_seconds()
         else:
-            return (int(part[0]), None)
-        
-    def __offset(self, s):
+            total_seconds = (self.__offset.days * 24 * 60 * 60) + \
+                            (self.__offset.seconds)
+
+        hours = total_seconds // (60 * 60)
+        total_seconds -= hours * 60 * 60
+
+        minutes = total_seconds // 60
+        total_seconds -= minutes * 60
+
+        seconds = total_seconds // 1
+        total_seconds -= seconds
+
+        if seconds:
+            return "%+03d:%02d:%02d" % (hours, minutes, seconds)
+        return "%+03d:%02d" % (hours, minutes)
+
+    def __unicode__(self):
+        return "FixedOffsetTimezone %s" % (self.tzname(None),)
+
+
+class UtcTimezone(FixedOffsetTimezone):
+    """
+    The UTC timezone.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
+    """
+
+    def __init__(self):
+        FixedOffsetTimezone.__init__(self, datetime.timedelta(0))
+
+    def tzname(self, dt):
         """
-        Parse the TZ offset.
-        @param s: A string representation of the TZ offset.
-        @type s: str
-        @return: The signed offset in hours.
-        @rtype: str
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
         """
-        if len(s) == len('-00:00'):
-            return int(s[:3])
-        if len(s) == 0:
-            return self.tz.local
-        if len(s) == 1:
-            return 0
-        raise Exception()
-
-    def __str__(self):
-        return unicode(self)
-    
+        return "UTC"
+
     def __unicode__(self):
-        time = self.time.isoformat()
-        if self.tz.local:
-            return '%s%+.2d:00' % (time, self.tz.local)
-        else:
-            return '%sZ' % time
+        return "UtcTimezone"
 
 
-class DateTime(Date,Time):
+class LocalTimezone(datetime.tzinfo):
     """
-    An XML time object.
-    Supported formats:
-        - YYYY-MM-DDB{T}HH:MI:SS
-        - YYYY-MM-DDB{T}HH:MI:SS(z|Z)
-        - YYYY-MM-DDB{T}HH:MI:SS.ms
-        - YYYY-MM-DDB{T}HH:MI:SS.ms(z|Z)
-        - YYYY-MM-DDB{T}HH:MI:SS(+|-)06:00
-        - YYYY-MM-DDB{T}HH:MI:SS.ms(+|-)06:00
-    @ivar datetime: The object value.
-    @type datetime: B{datetime}.I{datedate}
+    The local timezone of the operating system.
+
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+
     """
-    def __init__(self, date):
+
+    def __init__(self):
+        self.__offset = datetime.timedelta(seconds=-time.timezone)
+        self.__dst_offset = None
+        if time.daylight:
+            self.__dst_offset = datetime.timedelta(seconds=-time.altzone)
+
+    def dst(self, dt):
         """
-        @param date: The value of the object.
-        @type date: (datetime|str)
-        @raise ValueError: When I{tm} is invalid.
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.dst
+
+        """
+        if self.__is_daylight_time(dt):
+            return self.__dst_offset - self.__offset
+        return datetime.timedelta(0)
+
+    def tzname(self, dt):
+        """
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.tzname
+
         """
-        if isinstance(date, dt.datetime):
-            Date.__init__(self, date.date())
-            Time.__init__(self, date.time())
-            self.datetime = \
-                dt.datetime.combine(self.date, self.time)
-            return
-        if isinstance(date, basestring):
-            part = date.split('T')
-            Date.__init__(self, part[0])
-            Time.__init__(self, part[1], 0)
-            self.datetime = \
-                dt.datetime.combine(self.date, self.time)
-            self.__adjust()
-            return
-        raise ValueError, type(date)
-    
-    def __adjust(self):
+        if self.__is_daylight_time(dt):
+            return time.tzname[1]
+        return time.tzname[0]
+
+    def utcoffset(self, dt):
         """
-        Adjust for TZ offset.
+        http://docs.python.org/library/datetime.html#datetime.tzinfo.utcoffset
+
         """
-        if not hasattr(self, 'offset'):
-            return
-        delta = self.tz.adjustment(self.offset)
-        try:
-            d = ( self.datetime + delta )
-            self.datetime = d
-            self.date = d.date()
-            self.time = d.time()
-        except OverflowError:
-            log.warn('"%s" caused overflow, not-adjusted', self.datetime)
-
-    def __str__(self):
-        return unicode(self)
-    
+        if self.__is_daylight_time(dt):
+            return self.__dst_offset
+        return self.__offset
+
+    def __is_daylight_time(self, dt):
+        if not time.daylight:
+            return False
+        time_tuple = dt.replace(tzinfo=None).timetuple()
+        time_tuple = time.localtime(time.mktime(time_tuple))
+        return time_tuple.tm_isdst > 0
+
     def __unicode__(self):
-        s = []
-        s.append(Date.__unicode__(self))
-        s.append(Time.__unicode__(self))
-        return 'T'.join(s)
-    
-    
-class UTC(DateTime):
+        dt = datetime.datetime.now()
+        return "LocalTimezone %s offset: %s dst: %s" % (self.tzname(dt),
+            self.utcoffset(dt), self.dst(dt))
+
+
+def _bump_up_time_by_microsecond(time):
     """
-    Represents current UTC time.
+    Helper function bumping up the given datetime.time by a microsecond,
+    cycling around silently to 00:00:00.0 in case of an overflow.
+
+    @param time: Time object.
+    @type value: B{datetime}.I{time}
+    @return: Time object.
+    @rtype: B{datetime}.I{time}
+
     """
-    
-    def __init__(self, date=None):
-        if date is None:
-            date = dt.datetime.utcnow()
-        DateTime.__init__(self, date)
-        self.tz.local = 0
-    
-    
-class Timezone:
+    dt = datetime.datetime(2000, 1, 1, time.hour, time.minute,
+        time.second, time.microsecond)
+    dt += datetime.timedelta(microseconds=1)
+    return dt.time()
+
+
+def _date_from_match(match_object):
     """
-    Timezone object used to do TZ conversions
-    @cvar local: The (A) local TZ offset.
-    @type local: int
-    @cvar patten: The regex patten to match TZ.
-    @type patten: re.Pattern
+    Create a date object from a regular expression match.
+
+    The regular expression match is expected to be from _RE_DATE or
+    _RE_DATETIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: A date object.
+    @rtype: B{datetime}.I{date}
+
     """
-    
-    pattern = re.compile('([zZ])|([\-\+][0-9]{2}:[0-9]{2})')
-    
-    LOCAL = ( 0-time.timezone/60/60 )
-
-    def __init__(self, offset=None):
-        if offset is None:
-            offset = self.LOCAL
-        self.local = offset
-    
-    @classmethod
-    def split(cls, s):
-        """
-        Split the TZ from string.
-        @param s: A string containing a timezone
-        @type s: basestring
-        @return: The split parts.
-        @rtype: tuple
-        """
-        m = cls.pattern.search(s)
-        if m is None:
-            return (s,)
-        x = m.start(0)
-        return (s[:x], s[x:])
+    year = int(match_object.group("year"))
+    month = int(match_object.group("month"))
+    day = int(match_object.group("day"))
+    return datetime.date(year, month, day)
 
-    def adjustment(self, offset):
-        """
-        Get the adjustment to the I{local} TZ.
-        @return: The delta between I{offset} and local TZ.
-        @rtype: B{datetime}.I{timedelta}
-        """
-        delta = ( self.local - offset )
-        return dt.timedelta(hours=delta)
+
+def _time_from_match(match_object):
+    """
+    Create a time object from a regular expression match.
+
+    Returns the time object and information whether the resulting time should
+    be bumped up by one microsecond due to microsecond rounding.
+
+    Subsecond information is rounded to microseconds due to a restriction in
+    the python datetime.datetime/time implementation.
+
+    The regular expression match is expected to be from _RE_DATETIME or
+    _RE_TIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: Time object + rounding flag.
+    @rtype: tuple of B{datetime}.I{time} and bool
+
+    """
+    hour = int(match_object.group('hour'))
+    minute = int(match_object.group('minute'))
+    second = int(match_object.group('second'))
+    subsecond = match_object.group('subsecond')
+
+    round_up = False
+    microsecond = 0
+    if subsecond:
+        round_up = len(subsecond) > 6 and int(subsecond[6]) >= 5
+        subsecond = subsecond[:6]
+        microsecond = int(subsecond + "0" * (6 - len(subsecond)))
+    return datetime.time(hour, minute, second, microsecond), round_up
+
+
+def _tzinfo_from_match(match_object):
+    """
+    Create a timezone information object from a regular expression match.
+
+    The regular expression match is expected to be from _RE_DATE, _RE_DATETIME
+    or _RE_TIME.
+
+    @param match_object: The regular expression match.
+    @type value: B{re}.I{MatchObject}
+    @return: A timezone information object.
+    @rtype: B{datetime}.I{tzinfo}
+
+    """
+    tz_utc = match_object.group("tz_utc")
+    if tz_utc:
+        return UtcTimezone()
+
+    tz_sign = match_object.group("tz_sign")
+    if not tz_sign:
+        return
+
+    h = int(match_object.group("tz_hour") or 0)
+    m = int(match_object.group("tz_minute") or 0)
+    if h == 0 and m == 0:
+        return UtcTimezone()
+
+    # Python limitation - timezone offsets larger than one day (in absolute)
+    # will cause operations depending on tzinfo.utcoffset() to fail, e.g.
+    # comparing two timezone aware datetime.datetime/time objects.
+    if h >= 24:
+        raise ValueError("timezone indicator too large")
+
+    tz_delta = datetime.timedelta(hours=h, minutes=m)
+    if tz_sign == "-":
+        tz_delta *= -1
+    return FixedOffsetTimezone(tz_delta)
diff --git a/suds/sax/document.py b/suds/sax/document.py
index 5a004eb..7a4a615 100644
--- a/suds/sax/document.py
+++ b/suds/sax/document.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,44 +18,159 @@
 Provides XML I{document} classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.sax import *
 from suds.sax.element import Element
 
-log = getLogger(__name__)
 
-class Document(Element):
-    """ simple document """
-    
+class Document:
+    """ An XML Document """
+
     DECL = '<?xml version="1.0" encoding="UTF-8"?>'
 
     def __init__(self, root=None):
-        Element.__init__(self, 'document')
-        if root is not None:
-            self.append(root)
-        
+        """
+        @param root: A root L{Element} or name used to build
+            the document root element.
+        @type root: (L{Element}|str|None)
+        """
+        self.__root = None
+        self.append(root)
+
     def root(self):
-        if len(self.children):
-            return self.children[0]
+        """
+        Get the document root element (can be None)
+        @return: The document root.
+        @rtype: L{Element}
+        """
+        return self.__root
+
+    def append(self, node):
+        """
+        Append (set) the document root.
+        @param node: A root L{Element} or name used to build
+            the document root element.
+        @type node: (L{Element}|str|None)
+        """
+        if isinstance(node, basestring):
+            self.__root = Element(node)
+            return
+        if isinstance(node, Element):
+            self.__root = node
+            return
+
+    def getChild(self, name, ns=None, default=None):
+        """
+        Get a child by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @param default: Returned when child not-found.
+        @type default: L{Element}
+        @return: The requested child, or I{default} when not-found.
+        @rtype: L{Element}
+        """
+        if self.__root is None:
+            return default
+        if ns is None:
+            prefix, name = splitPrefix(name)
+            if prefix is None:
+                ns = None
+            else:
+                ns = self.__root.resolvePrefix(prefix)
+        if self.__root.match(name, ns):
+            return self.__root
         else:
+            return default
+
+    def childAtPath(self, path):
+        """
+        Get a child at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The leaf node at the end of I{path}
+        @rtype: L{Element}
+        """
+        if self.__root is None:
+            return None
+        if path[0] == '/':
+            path = path[1:]
+        path = path.split('/',1)
+        if self.getChild(path[0]) is None:
             return None
-        
+        if len(path) > 1:
+            return self.__root.childAtPath(path[1])
+        else:
+            return self.__root
+
+    def childrenAtPath(self, path):
+        """
+        Get a list of children at I{path} where I{path} is a (/) separated
+        list of element names that are expected to be children.
+        @param path: A (/) separated list of element names.
+        @type path: basestring
+        @return: The collection leaf nodes at the end of I{path}
+        @rtype: [L{Element},...]
+        """
+        if self.__root is None:
+            return []
+        if path[0] == '/':
+            path = path[1:]
+        path = path.split('/',1)
+        if self.getChild(path[0]) is None:
+            return []
+        if len(path) > 1:
+            return self.__root.childrenAtPath(path[1])
+        else:
+            return [self.__root,]
+
+    def getChildren(self, name=None, ns=None):
+        """
+        Get a list of children by (optional) name and/or (optional) namespace.
+        @param name: The name of a child element (may contain prefix).
+        @type name: basestring
+        @param ns: An optional namespace used to match the child.
+        @type ns: (I{prefix}, I{name})
+        @return: The list of matching children.
+        @rtype: [L{Element},...]
+        """
+        if name is None:
+            matched = self.__root
+        else:
+            matched = self.getChild(name, ns)
+        if matched is None:
+            return []
+        else:
+            return [matched,]
+
     def str(self):
+        """
+        Get a string representation of this XML document.
+        @return: A I{pretty} string.
+        @rtype: basestring
+        """
         s = []
         s.append(self.DECL)
-        s.append('\n')
-        s.append(self.root().str())
+        root = self.root()
+        if root is not None:
+            s.append('\n')
+            s.append(root.str())
         return ''.join(s)
-    
+
     def plain(self):
+        """
+        Get a string representation of this XML document.
+        @return: A I{plain} string.
+        @rtype: basestring
+        """
         s = []
         s.append(self.DECL)
-        s.append(self.root().plain())
+        root = self.root()
+        if root is not None:
+            s.append(root.plain())
         return ''.join(s)
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-    
     def __unicode__(self):
-        return self.str()
\ No newline at end of file
+        return self.str()
diff --git a/suds/sax/element.py b/suds/sax/element.py
index 9dec1f9..084ea2b 100644
--- a/suds/sax/element.py
+++ b/suds/sax/element.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,19 +18,13 @@
 Provides XML I{element} classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.sax import *
 from suds.sax.text import Text
 from suds.sax.attribute import Attribute
-import sys 
-if sys.version_info < (2, 4, 0): 
-    from sets import Set as set 
-    del sys 
 
-log = getLogger(__name__)
 
-class Element:
+class Element(UnicodeMixin):
     """
     An XML element object.
     @ivar parent: The node containing this attribute
@@ -53,21 +47,19 @@ class Element:
     @cvar specialprefixes: A dictionary of builtin-special prefixes.
     """
 
-    matcher = \
-    {
+    matcher = {
         'eq': lambda a,b: a == b,
         'startswith' : lambda a,b: a.startswith(b),
         'endswith' : lambda a,b: a.endswith(b),
-        'contains' : lambda a,b: b in a 
-    }
-    
-    specialprefixes = { Namespace.xmlns[0] : Namespace.xmlns[1]  }
-    
+        'contains' : lambda a,b: b in a}
+
+    specialprefixes = {Namespace.xmlns[0] : Namespace.xmlns[1]}
+
     @classmethod
     def buildPath(self, parent, path):
-        """
-        Build the specifed pat as a/b/c where missing intermediate nodes are built
-        automatically.
+        """Build the specifed path as a/b/c.
+
+        Any missing intermediate nodes are built automatically.
         @param parent: A parent element on which the path is built.
         @type parent: I{Element}
         @param path: A simple path separated by (/).
@@ -84,14 +76,13 @@ class Element:
 
     def __init__(self, name, parent=None, ns=None):
         """
-        @param name: The element's (tag) name.  May cotain a prefix.
+        @param name: The element's (tag) name. May contain a prefix.
         @type name: basestring
         @param parent: An optional parent element.
         @type parent: I{Element}
-        @param ns: An optional namespace
+        @param ns: An optional namespace.
         @type ns: (I{prefix}, I{name})
         """
-        
         self.rename(name)
         self.expns = None
         self.nsprefixes = {}
@@ -106,23 +97,23 @@ class Element:
             self.parent = None
         self.children = []
         self.applyns(ns)
-        
+
     def rename(self, name):
         """
         Rename the element.
         @param name: A new name for the element.
-        @type name: basestring 
+        @type name: basestring
         """
         if name is None:
             raise Exception('name (%s) not-valid' % name)
         else:
             self.prefix, self.name = splitPrefix(name)
-            
+
     def setPrefix(self, p, u=None):
         """
         Set the element namespace prefix.
         @param p: A new prefix for the element.
-        @type p: basestring 
+        @type p: basestring
         @param u: A namespace URI to be mapped to the prefix.
         @type u: basestring
         @return: self
@@ -130,6 +121,7 @@ class Element:
         """
         self.prefix = p
         if p is not None and u is not None:
+            self.expns = None
             self.addPrefix(p, u)
         return self
 
@@ -141,9 +133,8 @@ class Element:
         """
         if self.prefix is None:
             return self.name
-        else:
-            return '%s:%s' % (self.prefix, self.name)
-        
+        return '%s:%s' % (self.prefix, self.name)
+
     def getRoot(self):
         """
         Get the root (top) node of the tree.
@@ -152,9 +143,8 @@ class Element:
         """
         if self.parent is None:
             return self
-        else:
-            return self.parent.getRoot()
-        
+        return self.parent.getRoot()
+
     def clone(self, parent=None):
         """
         Deep clone of this element and children.
@@ -171,7 +161,7 @@ class Element:
         for item in self.nsprefixes.items():
             root.addPrefix(item[0], item[1])
         return root
-    
+
     def detach(self):
         """
         Detach from parent.
@@ -184,7 +174,7 @@ class Element:
                 self.parent.children.remove(self)
             self.parent = None
         return self
-        
+
     def set(self, name, value):
         """
         Set an attribute's value.
@@ -200,7 +190,7 @@ class Element:
             self.append(attr)
         else:
             attr.setValue(value)
-            
+
     def unset(self, name):
         """
         Unset (remove) an attribute.
@@ -215,8 +205,7 @@ class Element:
         except:
             pass
         return self
-            
-            
+
     def get(self, name, ns=None, default=None):
         """
         Get the value of an attribute by name.
@@ -234,8 +223,7 @@ class Element:
         attr = self.getAttribute(name, ns)
         if attr is None or attr.value is None:
             return default
-        else:
-            return attr.getValue()   
+        return attr.getValue()
 
     def setText(self, value):
         """
@@ -250,7 +238,7 @@ class Element:
         else:
             self.text = Text(value)
         return self
-        
+
     def getText(self, default=None):
         """
         Get the element's L{Text} content with optional default
@@ -261,9 +249,8 @@ class Element:
         """
         if self.hasText():
             return self.text
-        else:
-            return default
-    
+        return default
+
     def trim(self):
         """
         Trim leading and trailing whitespace.
@@ -273,7 +260,7 @@ class Element:
         if self.hasText():
             self.text = self.text.trim()
         return self
-    
+
     def hasText(self):
         """
         Get whether the element has I{text} and that it is not an empty
@@ -281,23 +268,22 @@ class Element:
         @return: True when has I{text}.
         @rtype: boolean
         """
-        return ( self.text is not None and len(self.text) )
-        
+        return self.text is not None and len(self.text)
+
     def namespace(self):
         """
         Get the element's namespace.
-        @return: The element's namespace by resolving the prefix, the explicit 
+        @return: The element's namespace by resolving the prefix, the explicit
             namespace or the inherited namespace.
         @rtype: (I{prefix}, I{name})
         """
         if self.prefix is None:
             return self.defaultNamespace()
-        else:
-            return self.resolvePrefix(self.prefix)
-        
+        return self.resolvePrefix(self.prefix)
+
     def defaultNamespace(self):
         """
-        Get the default (unqualified namespace).  
+        Get the default (unqualified namespace).
         This is the expns of the first node (looking up the tree)
         that has it set.
         @return: The namespace of a node when not qualified.
@@ -306,15 +292,14 @@ class Element:
         p = self
         while p is not None:
             if p.expns is not None:
-                return (None, p.expns)
-            else:
-                p = p.parent
+                return None, p.expns
+            p = p.parent
         return Namespace.default
-            
+
     def append(self, objects):
         """
-        Append the specified child based on whether it is an
-        element or an attrbuite.
+        Append the specified child based on whether it is an element or an
+        attribute.
         @param objects: A (single|collection) of attribute(s) or element(s)
             to be added as children.
         @type objects: (L{Element}|L{Attribute})
@@ -334,12 +319,12 @@ class Element:
                 continue
             raise Exception('append %s not-valid' % child.__class__.__name__)
         return self
-    
+
     def insert(self, objects, index=0):
         """
         Insert an L{Element} content at the specified index.
-        @param objects: A (single|collection) of attribute(s) or element(s)
-            to be added as children.
+        @param objects: A (single|collection) of attribute(s) or element(s) to
+            be added as children.
         @type objects: (L{Element}|L{Attribute})
         @param index: The position in the list of children to insert.
         @type index: int
@@ -354,7 +339,7 @@ class Element:
             else:
                 raise Exception('append %s not-valid' % child.__class__.__name__)
         return self
-    
+
     def remove(self, child):
         """
         Remove the specified child element or attribute.
@@ -368,7 +353,7 @@ class Element:
         if isinstance(child, Attribute):
             self.attributes.remove(child)
         return None
-            
+
     def replaceChild(self, child, content):
         """
         Replace I{child} with the specified I{content}.
@@ -387,7 +372,7 @@ class Element:
             self.children.insert(index, node.detach())
             node.parent = self
             index += 1
-            
+
     def getAttribute(self, name, ns=None, default=None):
         """
         Get an attribute by name and (optional) namespace
@@ -433,7 +418,7 @@ class Element:
             if c.match(name, ns):
                 return c
         return default
-    
+
     def childAtPath(self, path):
         """
         Get a child at I{path} where I{path} is a (/) separated
@@ -472,7 +457,7 @@ class Element:
         else:
             result = self.__childrenAtPath(parts)
         return result
-        
+
     def getChildren(self, name=None, ns=None):
         """
         Get a list of children by (optional) name and/or (optional) namespace.
@@ -492,7 +477,7 @@ class Element:
             else:
                 ns = self.resolvePrefix(prefix)
         return [c for c in self.children if c.match(name, ns)]
-    
+
     def detachChildren(self):
         """
         Detach and return this element's children.
@@ -504,7 +489,7 @@ class Element:
         for child in detached:
             child.parent = None
         return detached
-        
+
     def resolvePrefix(self, prefix, default=Namespace.default):
         """
         Resolve the specified prefix to a namespace.  The I{nsprefixes} is
@@ -522,12 +507,12 @@ class Element:
         n = self
         while n is not None:
             if prefix in n.nsprefixes:
-                return (prefix, n.nsprefixes[prefix])
+                return prefix, n.nsprefixes[prefix]
             if prefix in self.specialprefixes:
-                return (prefix, self.specialprefixes[prefix])
+                return prefix, self.specialprefixes[prefix]
             n = n.parent
         return default
-    
+
     def addPrefix(self, p, u):
         """
         Add or update a prefix mapping.
@@ -540,10 +525,10 @@ class Element:
         """
         self.nsprefixes[p] = u
         return self
- 
+
     def updatePrefix(self, p, u):
         """
-        Update (redefine) a prefix mapping for the branch. 
+        Update (redefine) a prefix mapping for the branch.
         @param p: A prefix.
         @type p: basestring
         @param u: A namespace URI.
@@ -557,7 +542,7 @@ class Element:
         for c in self.children:
             c.updatePrefix(p, u)
         return self
-            
+
     def clearPrefix(self, prefix):
         """
         Clear the specified prefix from the prefix mappings.
@@ -569,7 +554,7 @@ class Element:
         if prefix in self.nsprefixes:
             del self.nsprefixes[prefix]
         return self
-    
+
     def findPrefix(self, uri, default=None):
         """
         Find the first prefix that has been mapped to a namespace URI.
@@ -589,17 +574,16 @@ class Element:
         for item in self.specialprefixes.items():
             if item[1] == uri:
                 prefix = item[0]
-                return prefix      
+                return prefix
         if self.parent is not None:
             return self.parent.findPrefix(uri, default)
-        else:
-            return default
+        return default
 
     def findPrefixes(self, uri, match='eq'):
         """
-        Find all prefixes that has been mapped to a namespace URI.
-        The local mapping is searched, then it walks up the tree until
-        it reaches the top collecting all matches.
+        Find all prefixes that have been mapped to a namespace URI.
+        The local mapping is searched, then it walks up the tree until it
+        reaches the top, collecting all matches.
         @param uri: A namespace URI.
         @type uri: basestring
         @param match: A matching function L{Element.matcher}.
@@ -619,7 +603,7 @@ class Element:
         if self.parent is not None:
             result += self.parent.findPrefixes(uri, match)
         return result
-    
+
     def promotePrefixes(self):
         """
         Push prefix declarations up the tree as far as possible.  Prefix
@@ -643,7 +627,7 @@ class Element:
                 self.parent.nsprefixes[p] = u
                 del self.nsprefixes[p]
         return self
-    
+
     def refitPrefixes(self):
         """
         Refit namespace qualification by replacing prefixes
@@ -660,7 +644,7 @@ class Element:
         self.prefix = None
         self.nsprefixes = {}
         return self
-                
+
     def normalizePrefixes(self):
         """
         Normalize the namespace prefixes.
@@ -687,10 +671,8 @@ class Element:
         nocontent = ( nochildren and notext )
         if content:
             return nocontent
-        else:
-            return ( nocontent and noattrs )
-            
-            
+        return nocontent and noattrs
+
     def isnil(self):
         """
         Get whether the element is I{nil} as defined by having
@@ -699,16 +681,13 @@ class Element:
         @rtype: boolean
         """
         nilattr = self.getAttribute('nil', ns=Namespace.xsins)
-        if nilattr is None:
-            return False
-        else:
-            return ( nilattr.getValue().lower() == 'true' )
-        
+        return nilattr is not None and ( nilattr.getValue().lower() == 'true' )
+
     def setnil(self, flag=True):
         """
         Set this node to I{nil} as defined by having an
         attribute I{xsi:nil}=I{flag}.
-        @param flag: A flag inidcating how I{xsi:nil} will be set.
+        @param flag: A flag indicating how I{xsi:nil} will be set.
         @type flag: boolean
         @return: self
         @rtype: L{Element}
@@ -720,7 +699,7 @@ class Element:
         if flag:
             self.text = None
         return self
-            
+
     def applyns(self, ns):
         """
         Apply the namespace to this node.  If the prefix is I{None} then
@@ -731,14 +710,14 @@ class Element:
         """
         if ns is None:
             return
-        if not isinstance(ns, (tuple,list)):
+        if not isinstance(ns, (tuple, list)):
             raise Exception('namespace must be tuple')
         if ns[0] is None:
             self.expns = ns[1]
         else:
             self.prefix = ns[0]
             self.nsprefixes[ns[0]] = ns[1]
-            
+
     def str(self, indent=0):
         """
         Get a string representation of this XML fragment.
@@ -765,9 +744,8 @@ class Element:
         if len(self.children):
             result.append('\n%s' % tab)
         result.append('</%s>' % self.qname())
-        result = ''.join(result)
-        return result
-    
+        return ''.join(result)
+
     def plain(self):
         """
         Get a string representation of this XML fragment.
@@ -788,8 +766,7 @@ class Element:
         for c in self.children:
             result.append(c.plain())
         result.append('</%s>' % self.qname())
-        result = ''.join(result)
-        return result
+        return ''.join(result)
 
     def nsdeclarations(self):
         """
@@ -816,7 +793,7 @@ class Element:
             d = ' xmlns:%s="%s"' % (p, u)
             s.append(d)
         return ''.join(s)
-    
+
     def match(self, name=None, ns=None):
         """
         Match by (optional) name and/or (optional) namespace.
@@ -827,16 +804,10 @@ class Element:
         @return: True if matched.
         @rtype: boolean
         """
-        if name is None:
-            byname = True
-        else:
-            byname = ( self.name == name )
-        if ns is None:
-            byns = True
-        else:
-            byns = ( self.namespace()[1] == ns[1] )
-        return ( byname and byns )
-    
+        byname = name is None or ( self.name == name )
+        byns = ns is None or ( self.namespace()[1] == ns[1] )
+        return byname and byns
+
     def branch(self):
         """
         Get a flattened representation of the branch.
@@ -847,7 +818,7 @@ class Element:
         for c in self.children:
             branch += c.branch()
         return branch
-    
+
     def ancestors(self):
         """
         Get a list of ancestors.
@@ -860,11 +831,10 @@ class Element:
             ancestors.append(p)
             p = p.parent
         return ancestors
-    
+
     def walk(self, visitor):
         """
-        Walk the branch and call the visitor function
-        on each node.
+        Walk the branch and call the visitor function on each node.
         @param visitor: A function.
         @return: self
         @rtype: L{Element}
@@ -873,7 +843,7 @@ class Element:
         for c in self.children:
             c.walk(visitor)
         return self
-    
+
     def prune(self):
         """
         Prune the branch of empty nodes.
@@ -885,8 +855,7 @@ class Element:
                 pruned.append(c)
         for p in pruned:
             self.children.remove(p)
-                
-            
+
     def __childrenAtPath(self, parts):
         result = []
         node = self
@@ -910,46 +879,36 @@ class Element:
                 ns = node.resolvePrefix(prefix)
             result = child.getChildren(leaf)
         return result
-    
+
     def __len__(self):
         return len(self.children)
-                
+
     def __getitem__(self, index):
         if isinstance(index, basestring):
             return self.get(index)
-        else:
-            if index < len(self.children):
-                return self.children[index]
-            else:
-                return None
-        
+        if index < len(self.children):
+            return self.children[index]
+
     def __setitem__(self, index, value):
         if isinstance(index, basestring):
             self.set(index, value)
         else:
-            if index < len(self.children) and \
-                isinstance(value, Element):
+            if index < len(self.children) and isinstance(value, Element):
                 self.children.insert(index, value)
 
     def __eq__(self, rhs):
-        return  rhs is not None and \
-            isinstance(rhs, Element) and \
-            self.name == rhs.name and \
-            self.namespace()[1] == rhs.namespace()[1]
-        
+        return isinstance(rhs, Element) and  \
+            self.match(rhs.name, rhs.namespace())
+
     def __repr__(self):
-        return \
-            'Element (prefix=%s, name=%s)' % (self.prefix, self.name)
-    
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-    
+        return 'Element (prefix=%s, name=%s)' % (self.prefix, self.name)
+
     def __unicode__(self):
         return self.str()
-    
+
     def __iter__(self):
         return NodeIterator(self)
-    
+
 
 class NodeIterator:
     """
@@ -957,9 +916,9 @@ class NodeIterator:
     @ivar pos: The current position
     @type pos: int
     @ivar children: A list of a child nodes.
-    @type children: [L{Element},..] 
+    @type children: [L{Element},..]
     """
-    
+
     def __init__(self, parent):
         """
         @param parent: An element to iterate.
@@ -967,7 +926,7 @@ class NodeIterator:
         """
         self.pos = 0
         self.children = parent.children
-        
+
     def next(self):
         """
         Get the next child.
@@ -995,7 +954,7 @@ class PrefixNormalizer:
     @ivar prefixes: A reverse dict of prefixes.
     @type prefixes: {u, p}
     """
-    
+
     @classmethod
     def apply(cls, node):
         """
@@ -1007,7 +966,7 @@ class PrefixNormalizer:
         """
         pn = PrefixNormalizer(node)
         return pn.refit()
-    
+
     def __init__(self, node):
         """
         @param node: A node to normalize.
@@ -1017,7 +976,7 @@ class PrefixNormalizer:
         self.branch = node.branch()
         self.namespaces = self.getNamespaces()
         self.prefixes = self.genPrefixes()
-        
+
     def getNamespaces(self):
         """
         Get the I{unique} set of namespaces referenced in the branch.
@@ -1030,7 +989,7 @@ class PrefixNormalizer:
                 s.add(n.expns)
             s = s.union(self.pset(n))
         return s
-    
+
     def pset(self, n):
         """
         Convert the nodes nsprefixes into a set.
@@ -1044,7 +1003,7 @@ class PrefixNormalizer:
             if self.permit(ns):
                 s.add(ns[1])
         return s
-            
+
     def genPrefixes(self):
         """
         Generate a I{reverse} mapping of unique prefixes for all namespaces.
@@ -1058,14 +1017,14 @@ class PrefixNormalizer:
             prefixes[u] = p
             n += 1
         return prefixes
-    
+
     def refit(self):
         """
         Refit (normalize) the prefixes in the node.
         """
         self.refitNodes()
         self.refitMappings()
-    
+
     def refitNodes(self):
         """
         Refit (normalize) all of the nodes in the branch.
@@ -1076,7 +1035,7 @@ class PrefixNormalizer:
                 if self.permit(ns):
                     n.prefix = self.prefixes[ns[1]]
             self.refitAttrs(n)
-                
+
     def refitAttrs(self, n):
         """
         Refit (normalize) all of the attributes in the node.
@@ -1085,7 +1044,7 @@ class PrefixNormalizer:
         """
         for a in n.attributes:
             self.refitAddr(a)
-    
+
     def refitAddr(self, a):
         """
         Refit (normalize) the attribute.
@@ -1097,7 +1056,7 @@ class PrefixNormalizer:
             if self.permit(ns):
                 a.prefix = self.prefixes[ns[1]]
         self.refitValue(a)
-    
+
     def refitValue(self, a):
         """
         Refit (normalize) the attribute's value.
@@ -1111,7 +1070,7 @@ class PrefixNormalizer:
             u = ns[1]
             p = self.prefixes[u]
             a.setValue(':'.join((p, name)))
-            
+
     def refitMappings(self):
         """
         Refit (normalize) all of the nsprefix mappings.
@@ -1121,7 +1080,7 @@ class PrefixNormalizer:
         n = self.node
         for u, p in self.prefixes.items():
             n.addPrefix(p, u)
-            
+
     def permit(self, ns):
         """
         Get whether the I{ns} is to be normalized.
@@ -1131,7 +1090,7 @@ class PrefixNormalizer:
         @rtype: boolean
         """
         return not self.skip(ns)
-            
+
     def skip(self, ns):
         """
         Get whether the I{ns} is to B{not} be normalized.
@@ -1140,8 +1099,8 @@ class PrefixNormalizer:
         @return: True if to be skipped.
         @rtype: boolean
         """
-        return ns is None or \
-            ( ns == Namespace.default ) or \
-            ( ns == Namespace.xsdns ) or \
-            ( ns == Namespace.xsins) or \
-            ( ns == Namespace.xmlns )
\ No newline at end of file
+        return (ns is None or
+            ns == Namespace.default or
+            ns == Namespace.xsdns or
+            ns == Namespace.xsins or
+            ns == Namespace.xmlns)
diff --git a/suds/sax/enc.py b/suds/sax/enc.py
index efc7274..8d3219c 100644
--- a/suds/sax/enc.py
+++ b/suds/sax/enc.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -30,14 +30,14 @@ class Encoder:
     @cvar special: A list of special characters
     @type special: [char]
     """
-    
+
     encodings = \
         (( '&(?!(amp|lt|gt|quot|apos);)', '&' ),( '<', '<' ),( '>', '>' ),( '"', '"' ),("'", ''' ))
     decodings = \
         (( '<', '<' ),( '>', '>' ),( '"', '"' ),( ''', "'" ),( '&', '&' ))
     special = \
         ('&', '<', '>', '"', "'")
-    
+
     def needsEncoding(self, s):
         """
         Get whether string I{s} contains special characters.
@@ -51,7 +51,7 @@ class Encoder:
                 if c in s:
                     return True
         return False
-    
+
     def encode(self, s):
         """
         Encode special characters found in string I{s}.
@@ -64,7 +64,7 @@ class Encoder:
             for x in self.encodings:
                 s = re.sub(x[0], x[1], s)
         return s
-    
+
     def decode(self, s):
         """
         Decode special characters encodings found in string I{s}.
diff --git a/suds/sax/parser.py b/suds/sax/parser.py
index 69f871b..a82583a 100644
--- a/suds/sax/parser.py
+++ b/suds/sax/parser.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -17,7 +17,7 @@
 """
 The sax module contains a collection of classes that provide a
 (D)ocument (O)bject (M)odel representation of an XML document.
-The goal is to provide an easy, intuative interface for managing XML
+The goal is to provide an easy, intuitive interface for managing XML
 documents.  Although, the term, DOM, is used above, this model is
 B{far} better.
 
@@ -26,30 +26,28 @@ containing the prefix and the URI.  Eg: I{('tns', 'http://myns')}
 
 """
 
-from logging import getLogger
-import suds.metrics
+import suds
 from suds import *
 from suds.sax import *
+from suds.sax.attribute import Attribute
 from suds.sax.document import Document
 from suds.sax.element import Element
 from suds.sax.text import Text
-from suds.sax.attribute import Attribute
+
+import sys
 from xml.sax import make_parser, InputSource, ContentHandler
 from xml.sax.handler import feature_external_ges
-from cStringIO import StringIO
-
-log = getLogger(__name__)
 
 
 class Handler(ContentHandler):
     """ sax hanlder """
-    
+
     def __init__(self):
         self.nodes = [Document()]
- 
+
     def startElement(self, name, attrs):
         top = self.top()
-        node = Element(unicode(name), parent=top)
+        node = Element(unicode(name))
         for a in attrs.getNames():
             n = unicode(a)
             v = unicode(attrs.getValue(a))
@@ -60,7 +58,7 @@ class Handler(ContentHandler):
         node.charbuffer = []
         top.append(node)
         self.push(node)
-        
+
     def mapPrefix(self, node, attribute):
         skip = False
         if attribute.name == 'xmlns':
@@ -72,7 +70,7 @@ class Handler(ContentHandler):
             node.nsprefixes[prefix] = unicode(attribute.value)
             skip = True
         return skip
- 
+
     def endElement(self, name):
         name = unicode(name)
         current = self.top()
@@ -81,12 +79,11 @@ class Handler(ContentHandler):
         del current.charbuffer
         if len(current):
             current.trim()
-        currentqname = current.qname()
-        if name == currentqname:
+        if name == current.qname():
             self.pop()
         else:
             raise Exception('malformed document')
- 
+
     def characters(self, content):
         text = unicode(content)
         node = self.top()
@@ -98,14 +95,14 @@ class Handler(ContentHandler):
 
     def pop(self):
         return self.nodes.pop()
- 
+
     def top(self):
         return self.nodes[len(self.nodes)-1]
 
 
 class Parser:
     """ SAX Parser """
-    
+
     @classmethod
     def saxparser(cls):
         p = make_parser()
@@ -113,7 +110,7 @@ class Parser:
         h = Handler()
         p.setContentHandler(h)
         return (p, h)
-        
+
     def parse(self, file=None, string=None):
         """
         SAX parse XML text.
@@ -122,18 +119,18 @@ class Parser:
         @param string: Parse string XML.
         @type string: str
         """
-        timer = metrics.Timer()
+        timer = suds.metrics.Timer()
         timer.start()
         sax, handler = self.saxparser()
         if file is not None:
             sax.parse(file)
             timer.stop()
-            metrics.log.debug('sax (%s) duration: %s', file, timer)
+            suds.metrics.log.debug('sax (%s) duration: %s', file, timer)
             return handler.nodes[0]
         if string is not None:
             source = InputSource(None)
-            source.setByteStream(StringIO(string))
+            source.setByteStream(suds.BytesIO(string))
             sax.parse(source)
             timer.stop()
-            metrics.log.debug('%s\nsax duration: %s', string, timer)
-            return handler.nodes[0]
\ No newline at end of file
+            suds.metrics.log.debug('%s\nsax duration: %s', string, timer)
+            return handler.nodes[0]
diff --git a/suds/sax/text.py b/suds/sax/text.py
index 0d58ee8..985386e 100644
--- a/suds/sax/text.py
+++ b/suds/sax/text.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -30,12 +30,12 @@ class Text(unicode):
     @ivar escaped: The (optional) XML special character escaped flag.
     @type escaped: bool
     """
-    __slots__ = ('lang', 'escaped',)
-    
+    __slots__ = ('lang', 'escaped')
+
     @classmethod
     def __valid(cls, *args):
-        return ( len(args) and args[0] is not None )
-    
+        return len(args) and args[0] is not None
+
     def __new__(cls, *args, **kwargs):
         if cls.__valid(*args):
             lang = kwargs.pop('lang', None)
@@ -46,7 +46,7 @@ class Text(unicode):
         else:
             result = None
         return result
-    
+
     def escape(self):
         """
         Encode (escape) special XML characters.
@@ -58,7 +58,7 @@ class Text(unicode):
             escaped = ( post != self )
             return Text(post, lang=self.lang, escaped=escaped)
         return self
-    
+
     def unescape(self):
         """
         Decode (unescape) special XML characters.
@@ -69,18 +69,18 @@ class Text(unicode):
             post = sax.encoder.decode(self)
             return Text(post, lang=self.lang)
         return self
-    
+
     def trim(self):
         post = self.strip()
         return Text(post, lang=self.lang, escaped=self.escaped)
-    
+
     def __add__(self, other):
         joined = u''.join((self, other))
         result = Text(joined, lang=self.lang, escaped=self.escaped)
         if isinstance(other, Text):
-            result.escaped = ( self.escaped or other.escaped )
+            result.escaped = self.escaped or other.escaped
         return result
-    
+
     def __repr__(self):
         s = [self]
         if self.lang is not None:
@@ -88,18 +88,18 @@ class Text(unicode):
         if self.escaped:
             s.append(' <escaped>')
         return ''.join(s)
-    
+
     def __getstate__(self):
         state = {}
         for k in self.__slots__:
             state[k] = getattr(self, k)
         return state
-    
+
     def __setstate__(self, state):
         for k in self.__slots__:
             setattr(self, k, state[k])
-    
-    
+
+
 class Raw(Text):
     """
     Raw text which is not XML escaped.
@@ -107,10 +107,10 @@ class Raw(Text):
     """
     def escape(self):
         return self
-    
+
     def unescape(self):
         return self
-    
+
     def __add__(self, other):
         joined = u''.join((self, other))
         return Raw(joined, lang=self.lang)
diff --git a/suds/servicedefinition.py b/suds/servicedefinition.py
index 81b5a0d..6b0e72f 100644
--- a/suds/servicedefinition.py
+++ b/suds/servicedefinition.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,14 +18,15 @@
 The I{service definition} provides a textual representation of a service.
 """
 
-from logging import getLogger
 from suds import *
 import suds.metrics as metrics
 from suds.sax import Namespace
 
+from logging import getLogger
 log = getLogger(__name__)
 
-class ServiceDefinition:
+
+class ServiceDefinition(UnicodeMixin):
     """
     A service definition provides an object used to generate a textual description
     of a service.
@@ -38,12 +39,12 @@ class ServiceDefinition:
     @ivar prefixes: A list of remapped prefixes.
     @type prefixes: [(prefix,uri),..]
     @ivar types: A list of type definitions
-    @type types: [I{Type},..] 
+    @type types: [I{Type},..]
     """
-    
+
     def __init__(self, wsdl, service):
         """
-        @param wsdl: A wsdl object
+        @param wsdl: A WSDL object
         @type wsdl: L{Definitions}
         @param service: A service B{name}.
         @type service: str
@@ -59,21 +60,21 @@ class ServiceDefinition:
         self.publictypes()
         self.getprefixes()
         self.pushprefixes()
-    
+
     def pushprefixes(self):
         """
-        Add our prefixes to the wsdl so that when users invoke methods
-        and reference the prefixes, the will resolve properly.
+        Add our prefixes to the WSDL so that when users invoke methods
+        and reference the prefixes, they will resolve properly.
         """
         for ns in self.prefixes:
             self.wsdl.root.addPrefix(ns[0], ns[1])
 
     def addports(self):
         """
-        Look through the list of service ports and construct a list of tuples where
-        each tuple is used to describe a port and it's list of methods as:
-        (port, [method]).  Each method is tuple: (name, [pdef,..] where each pdef is
-        a tuple: (param-name, type).
+        Look through the list of service ports and construct a list of tuples
+        where each tuple is used to describe a port and its list of methods as:
+        (port, [method]).  Each method is a tuple: (name, [pdef,..]) where each
+        pdef is a tuple: (param-name, type).
         """
         timer = metrics.Timer()
         timer.start()
@@ -87,7 +88,7 @@ class ServiceDefinition:
                 metrics.log.debug("method '%s' created: %s", m.name, timer)
             p[1].sort()
         timer.stop()
-            
+
     def findport(self, port):
         """
         Find and return a port tuple for the specified port.
@@ -102,11 +103,9 @@ class ServiceDefinition:
         p = (port, [])
         self.ports.append(p)
         return p
-            
+
     def getprefixes(self):
-        """
-        Add prefixes foreach namespace referenced by parameter types.
-        """
+        """Add prefixes for each namespace referenced by parameter types."""
         namespaces = []
         for l in (self.params, self.types):
             for t,r in l:
@@ -127,31 +126,30 @@ class ServiceDefinition:
             p = self.nextprefix()
             ns = (p, u)
             self.prefixes.append(ns)
-            
+
     def paramtypes(self):
-        """ get all parameter types """
+        """Get all parameter types."""
         for m in [p[1] for p in self.ports]:
             for p in [p[1] for p in m]:
                 for pd in p:
                     if pd[1] in self.params: continue
                     item = (pd[1], pd[1].resolve())
                     self.params.append(item)
-                    
+
     def publictypes(self):
-        """ get all public types """
+        """Get all public types."""
         for t in self.wsdl.schema.types.values():
             if t in self.params: continue
             if t in self.types: continue
             item = (t, t)
             self.types.append(item)
-        tc = lambda x,y: cmp(x[0].name, y[0].name)
-        self.types.sort(cmp=tc)
-        
+        self.types.sort(key=lambda x: x[0].name)
+
     def nextprefix(self):
         """
         Get the next available prefix.  This means a prefix starting with 'ns' with
-        a number appended as (ns0, ns1, ..) that is not already defined on the
-        wsdl document.
+        a number appended as (ns0, ns1, ..) that is not already defined in the
+        WSDL document.
         """
         used = [ns[0] for ns in self.prefixes]
         used += [ns[0] for ns in self.wsdl.root.nsprefixes.items()]
@@ -160,11 +158,11 @@ class ServiceDefinition:
             if p not in used:
                 return p
         raise Exception('prefixes exhausted')
-    
+
     def getprefix(self, u):
         """
-        Get the prefix for the specified namespace (uri)
-        @param u: A namespace uri.
+        Get the prefix for the specified namespace (URI)
+        @param u: A namespace URI.
         @type u: str
         @return: The namspace.
         @rtype: (prefix, uri).
@@ -174,7 +172,7 @@ class ServiceDefinition:
         for ns in self.prefixes:
             if u == ns[1]: return ns[0]
         raise Exception('ns (%s) not mapped'  % u)
-    
+
     def xlate(self, type):
         """
         Get a (namespace) translated I{qualified} name for specified type.
@@ -185,14 +183,14 @@ class ServiceDefinition:
         """
         resolved = type.resolve()
         name = resolved.name
-        if type.unbounded():
+        if type.multi_occurrence():
             name += '[]'
         ns = resolved.namespace()
         if ns[1] == self.wsdl.tns[1]:
             return name
         prefix = self.getprefix(ns[1])
         return ':'.join((prefix, name))
-        
+
     def description(self):
         """
         Get a textual description of the service for which this object represents.
@@ -219,11 +217,8 @@ class ServiceDefinition:
                 s.append(indent(4))
                 sig.append(m[0])
                 sig.append('(')
-                for p in m[1]:
-                    sig.append(self.xlate(p[1]))
-                    sig.append(' ')
-                    sig.append(p[0])
-                    sig.append(', ')
+                sig.append(', '.join("%s %s" % (self.xlate(p[1]), p[0]) for p
+                    in m[1]))
                 sig.append(')')
                 try:
                     s.append(''.join(sig))
@@ -236,13 +231,10 @@ class ServiceDefinition:
                 s.append(self.xlate(t[0]))
         s.append('\n\n')
         return ''.join(s)
-    
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-        
+
     def __unicode__(self):
         try:
             return self.description()
         except Exception, e:
             log.exception(e)
-        return tostr(e)
\ No newline at end of file
+        return tostr(e)
diff --git a/suds/serviceproxy.py b/suds/serviceproxy.py
index 6e71050..278c189 100644
--- a/suds/serviceproxy.py
+++ b/suds/serviceproxy.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -20,18 +20,15 @@ The service proxy provides access to web services.
 Replaced by: L{client.Client}
 """
 
-from logging import getLogger
 from suds import *
 from suds.client import Client
 
-log = getLogger(__name__)
 
+class ServiceProxy(UnicodeMixin):
 
-class ServiceProxy(object):
-    
-    """ 
+    """
     A lightweight soap based web service proxy.
-    @ivar __client__: A client.  
+    @ivar __client__: A client.
         Everything is delegated to the 2nd generation API.
     @type __client__: L{Client}
     @note:  Deprecated, replaced by L{Client}.
@@ -51,7 +48,7 @@ class ServiceProxy(object):
         """
         client = Client(url, **kwargs)
         self.__client__ = client
-    
+
     def get_instance(self, name):
         """
         Get an instance of a WSDL type by name
@@ -61,7 +58,7 @@ class ServiceProxy(object):
         @rtype: L{sudsobject.Object}
         """
         return self.__client__.factory.create(name)
-    
+
     def get_enum(self, name):
         """
         Get an instance of an enumeration defined in the WSDL by name.
@@ -71,16 +68,13 @@ class ServiceProxy(object):
         @rtype: L{sudsobject.Object}
         """
         return self.__client__.factory.create(name)
- 
-    def __str__(self):
-        return str(self.__client__)
-        
+
     def __unicode__(self):
         return unicode(self.__client__)
-    
+
     def __getattr__(self, name):
-        builtin =  name.startswith('__') and name.endswith('__')
+        builtin = name.startswith('__') and name.endswith('__')
         if builtin:
             return self.__dict__[name]
         else:
-            return getattr(self.__client__.service, name)
\ No newline at end of file
+            return getattr(self.__client__.service, name)
diff --git a/suds/soaparray.py b/suds/soaparray.py
index 04847d5..ea04fa7 100644
--- a/suds/soaparray.py
+++ b/suds/soaparray.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -43,12 +43,12 @@ class Attribute(SXAttribute):
             self.aty = aty[:-2]
         else:
             self.aty = aty
-        
+
     def autoqualified(self):
         aqs = SXAttribute.autoqualified(self)
         aqs.append('aty')
         return aqs
-    
+
     def description(self):
         d = SXAttribute.description(self)
         d = d+('aty',)
@@ -63,10 +63,9 @@ def __fn(x, y):
     aty = y.get('arrayType', ns=ns)
     if aty is None:
         return SXAttribute(x, y)
-    else:
-        return Attribute(x, y, aty)
+    return Attribute(x, y, aty)
 
 #
-# Remap <xs:attrbute/> tags to __fn() builder.
+# Remap <xs:attribute/> tags to __fn() builder.
 #
-SXFactory.maptag('attribute', __fn)
\ No newline at end of file
+SXFactory.maptag('attribute', __fn)
diff --git a/suds/store.py b/suds/store.py
index 85e0943..e5931aa 100644
--- a/suds/store.py
+++ b/suds/store.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,30 +15,27 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains XML text for documents to be distributed
-with the suds lib.  Also, contains classes for accessing
-these documents.
+Support for holding XML document texts that may then be accessed internally by
+suds without having to download them from an external source. Also contains XML
+document content to be distributed alongside the suds library.
+
 """
 
-from StringIO import StringIO
-from logging import getLogger
+import suds
 
-log = getLogger(__name__)
 
+soap5_encoding_schema = suds.byte_str("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+    xmlns:tns="http://schemas.xmlsoap.org/soap/encoding/"
+    targetNamespace="http://schemas.xmlsoap.org/soap/encoding/">
 
-#
-# Soap section 5 encoding schema.
-#
-encoding = \
-"""<?xml version="1.0" encoding="UTF-8"?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.xmlsoap.org/soap/encoding/" targetNamespace="http://schemas.xmlsoap.org/soap/encoding/">
-        
  <xs:attribute name="root">
    <xs:annotation>
      <xs:documentation>
        'root' can be used to distinguish serialization roots from other
        elements that are present in a serialization but are not roots of
-       a serialized value graph 
+       a serialized value graph
      </xs:documentation>
    </xs:annotation>
    <xs:simpleType>
@@ -51,7 +48,7 @@ encoding = \
   <xs:attributeGroup name="commonAttributes">
     <xs:annotation>
       <xs:documentation>
-        Attributes common to all elements that function as accessors or 
+        Attributes common to all elements that function as accessors or
         represent independent (multi-ref) values.  The href attribute is
         intended to be used in a manner like CONREF.  That is, the element
         content should be empty iff the href attribute appears
@@ -63,26 +60,26 @@ encoding = \
   </xs:attributeGroup>
 
   <!-- Global Attributes.  The following attributes are intended to be usable via qualified attribute names on any complex type referencing them. -->
-       
+
   <!-- Array attributes. Needed to give the type and dimensions of an array's contents, and the offset for partially-transmitted arrays. -->
-   
+
   <xs:simpleType name="arrayCoordinate">
     <xs:restriction base="xs:string"/>
   </xs:simpleType>
-          
+
   <xs:attribute name="arrayType" type="xs:string"/>
   <xs:attribute name="offset" type="tns:arrayCoordinate"/>
-  
+
   <xs:attributeGroup name="arrayAttributes">
     <xs:attribute ref="tns:arrayType"/>
     <xs:attribute ref="tns:offset"/>
-  </xs:attributeGroup>    
-  
-  <xs:attribute name="position" type="tns:arrayCoordinate"/> 
-  
+  </xs:attributeGroup>
+
+  <xs:attribute name="position" type="tns:arrayCoordinate"/>
+
   <xs:attributeGroup name="arrayMemberAttributes">
     <xs:attribute ref="tns:position"/>
-  </xs:attributeGroup>    
+  </xs:attributeGroup>
 
   <xs:group name="Array">
     <xs:sequence>
@@ -94,18 +91,18 @@ encoding = \
   <xs:complexType name="Array">
     <xs:annotation>
       <xs:documentation>
-       'Array' is a complex type for accessors identified by position 
+       'Array' is a complex type for accessors identified by position
       </xs:documentation>
     </xs:annotation>
     <xs:group ref="tns:Array" minOccurs="0"/>
     <xs:attributeGroup ref="tns:arrayAttributes"/>
     <xs:attributeGroup ref="tns:commonAttributes"/>
-  </xs:complexType> 
+  </xs:complexType>
 
-  <!-- 'Struct' is a complex type for accessors identified by name. 
+  <!-- 'Struct' is a complex type for accessors identified by name.
        Constraint: No element may be have the same name as any other,
        nor may any element have a maxOccurs > 1. -->
-   
+
   <xs:element name="Struct" type="tns:Struct"/>
 
   <xs:group name="Struct">
@@ -117,7 +114,7 @@ encoding = \
   <xs:complexType name="Struct">
     <xs:group ref="tns:Struct" minOccurs="0"/>
     <xs:attributeGroup ref="tns:commonAttributes"/>
-  </xs:complexType> 
+  </xs:complexType>
 
   <!-- 'Base64' can be used to serialize binary data using base64 encoding
        as defined in RFC2045 but without the MIME line length limitation. -->
@@ -126,7 +123,7 @@ encoding = \
     <xs:restriction base="xs:base64Binary"/>
   </xs:simpleType>
 
- <!-- Element declarations corresponding to each of the simple types in the 
+ <!-- Element declarations corresponding to each of the simple types in the
       XML Schemas Specification. -->
 
   <xs:element name="duration" type="tns:duration"/>
@@ -147,8 +144,6 @@ encoding = \
     </xs:simpleContent>
   </xs:complexType>
 
-
-
   <xs:element name="NOTATION" type="tns:NOTATION"/>
   <xs:complexType name="NOTATION">
     <xs:simpleContent>
@@ -157,7 +152,6 @@ encoding = \
       </xs:extension>
     </xs:simpleContent>
   </xs:complexType>
-  
 
   <xs:element name="time" type="tns:time"/>
   <xs:complexType name="time">
@@ -221,7 +215,7 @@ encoding = \
       </xs:extension>
     </xs:simpleContent>
   </xs:complexType>
-  
+
   <xs:element name="boolean" type="tns:boolean"/>
   <xs:complexType name="boolean">
     <xs:simpleContent>
@@ -285,7 +279,6 @@ encoding = \
     </xs:simpleContent>
   </xs:complexType>
 
-  
   <xs:element name="string" type="tns:string"/>
   <xs:complexType name="string">
     <xs:simpleContent>
@@ -531,57 +524,64 @@ encoding = \
 
   <xs:element name="anyType"/>
 </xs:schema>
-"""
+""")
 
 
 class DocumentStore:
     """
-    The I{suds} document store provides a local repository
-    for xml documnts.
+    The I{suds} document store provides a local repository for XML documents.
+
     @cvar protocol: The URL protocol for the store.
     @type protocol: str
     @cvar store: The mapping of URL location to documents.
     @type store: dict
     """
-    
-    protocol = 'suds'
-    
-    store = {
-        'schemas.xmlsoap.org/soap/encoding/' : encoding
-    }
-    
+
+    def __init__(self, *args, **kwargs):
+        self.__store = {
+            'schemas.xmlsoap.org/soap/encoding/':soap5_encoding_schema}
+        self.update = self.__store.update
+        self.update(*args, **kwargs)
+
+    def __len__(self):
+        # Implementation note:
+        #   We can not implement '__len__' as simply self.__store.__len__, as
+        # we do for 'update' because that causes py2to3 conversion to fail.
+        #                                            (08.05.2013.) (Jurko)
+        return len(self.__store)
+
     def open(self, url):
         """
-        Open a document at the specified url.
+        Open a document at the specified URL.
+
+        Missing documents referenced using the internal 'suds' protocol are
+        reported by raising an exception. For other protocols, None is returned
+        instead.
+
         @param url: A document URL.
         @type url: str
-        @return: A file pointer to the document.
-        @rtype: StringIO
+        @return: Document content or None if not found.
+        @rtype: bytes
         """
-        protocol, location = self.split(url)
-        if protocol == self.protocol:
-            return self.find(location)
-        else:
-            return None
-        
-    def find(self, location):
+        protocol, location = self.__split(url)
+        content = self.__find(location)
+        if protocol == 'suds' and content is None:
+            raise Exception, 'location "%s" not in document store' % location
+        return content
+
+    def __find(self, location):
         """
         Find the specified location in the store.
         @param location: The I{location} part of a URL.
         @type location: str
-        @return: An input stream to the document.
-        @rtype: StringIO
+        @return: Document content or None if not found.
+        @rtype: bytes
         """
-        try:
-            content = self.store[location]
-            return StringIO(content)
-        except:
-            reason = 'location "%s" not in document store' % location
-            raise Exception, reason
-        
-    def split(self, url):
+        return self.__store.get(location)
+
+    def __split(self, url):
         """
-        Split the url into I{protocol} and I{location}
+        Split the URL into I{protocol} and I{location}
         @param url: A URL.
         @param url: str
         @return: (I{url}, I{location})
@@ -590,5 +590,7 @@ class DocumentStore:
         parts = url.split('://', 1)
         if len(parts) == 2:
             return parts
-        else:
-            return (None, url)
\ No newline at end of file
+        return None, url
+
+
+defaultDocumentStore = DocumentStore()
diff --git a/suds/sudsobject.py b/suds/sudsobject.py
index 1f6168d..f96d419 100644
--- a/suds/sudsobject.py
+++ b/suds/sudsobject.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -20,10 +20,9 @@ that are primarily used for the highly dynamic interactions with
 wsdl/xsd defined types.
 """
 
-from logging import getLogger
 from suds import *
-from new import classobj
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -86,23 +85,24 @@ def footprint(sobject):
         n +=1
     return n
 
-    
+
 class Factory:
-    
+
     cache = {}
-    
+
     @classmethod
     def subclass(cls, name, bases, dict={}):
         if not isinstance(bases, tuple):
             bases = (bases,)
-        name = name.encode('utf-8')
+        # name is type unicode in python 2 -> not accepted by the type()
+        name = str(name)
         key = '.'.join((name, str(bases)))
         subclass = cls.cache.get(key)
         if subclass is None:
-            subclass = classobj(name, bases, dict)
+            subclass = type(name, bases, dict)
             cls.cache[key] = subclass
         return subclass
-    
+
     @classmethod
     def object(cls, classname=None, dict={}):
         if classname is not None:
@@ -113,18 +113,18 @@ class Factory:
         for a in dict.items():
             setattr(inst, a[0], a[1])
         return inst
-    
+
     @classmethod
     def metadata(cls):
         return Metadata()
-    
+
     @classmethod
     def property(cls, name, value=None):
         subclass = cls.subclass(name, Property)
         return subclass(value)
 
 
-class Object:
+class Object(UnicodeMixin):
 
     def __init__(self):
         self.__keylist__ = []
@@ -132,16 +132,16 @@ class Object:
         self.__metadata__ = Metadata()
 
     def __setattr__(self, name, value):
-        builtin =  name.startswith('__') and name.endswith('__')
+        builtin = name.startswith('__') and name.endswith('__')
         if not builtin and \
             name not in self.__keylist__:
             self.__keylist__.append(name)
         self.__dict__[name] = value
-        
+
     def __delattr__(self, name):
         try:
             del self.__dict__[name]
-            builtin =  name.startswith('__') and name.endswith('__')
+            builtin = name.startswith('__') and name.endswith('__')
             if not builtin:
                 self.__keylist__.remove(name)
         except:
@@ -152,25 +152,22 @@ class Object:
         if isinstance(name, int):
             name = self.__keylist__[int(name)]
         return getattr(self, name)
-    
+
     def __setitem__(self, name, value):
         setattr(self, name, value)
-        
+
     def __iter__(self):
         return Iter(self)
 
     def __len__(self):
         return len(self.__keylist__)
-    
+
     def __contains__(self, name):
         return name in self.__keylist__
-    
+
     def __repr__(self):
         return str(self)
 
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-    
     def __unicode__(self):
         return self.__printer__.tostr(self)
 
@@ -192,7 +189,7 @@ class Iter:
                 v = getattr(self.sobject, k)
                 return (k, v)
         raise StopIteration()
-    
+
     def __keylist(self, sobject):
         keylist = sobject.__keylist__
         try:
@@ -202,13 +199,13 @@ class Iter:
             if not ordered.issuperset(keyset):
                 log.debug(
                     '%s must be superset of %s, ordering ignored',
-                    keylist, 
+                    keylist,
                     ordering)
                 raise KeyError()
             return ordering
         except:
             return keylist
-        
+
     def __iter__(self):
         return self
 
@@ -225,31 +222,31 @@ class Facade(Object):
         md = self.__metadata__
         md.facade = name
 
-       
+
 class Property(Object):
 
     def __init__(self, value):
         Object.__init__(self)
         self.value = value
-        
+
     def items(self):
         for item in self:
             if item[0] != 'value':
                 yield item
-        
+
     def get(self):
         return self.value
-    
+
     def set(self, value):
         self.value = value
         return self
 
 
 class Printer:
-    """ 
+    """
     Pretty printing of a Object object.
     """
-    
+
     @classmethod
     def indent(cls, n): return '%*s'%(n*3,' ')
 
@@ -257,7 +254,7 @@ class Printer:
         """ get s string representation of object """
         history = []
         return self.process(object, history, indent)
-    
+
     def process(self, object, h, n=0, nl=False):
         """ print object using the specified indent (n) and newline (nl). """
         if object is None:
@@ -265,27 +262,23 @@ class Printer:
         if isinstance(object, Object):
             if len(object) == 0:
                 return '<empty>'
-            else:
-                return self.print_object(object, h, n+2, nl)
+            return self.print_object(object, h, n+2, nl)
         if isinstance(object, dict):
             if len(object) == 0:
                 return '<empty>'
-            else:
-                return self.print_dictionary(object, h, n+2, nl)
+            return self.print_dictionary(object, h, n+2, nl)
         if isinstance(object, (list,tuple)):
             if len(object) == 0:
                 return '<empty>'
-            else:
-                return self.print_collection(object, h, n+2)
+            return self.print_collection(object, h, n+2)
         if isinstance(object, basestring):
             return '"%s"' % tostr(object)
         return '%s' % tostr(object)
-    
+
     def print_object(self, d, h, n, nl=False):
         """ print complex using the specified indent (n) and newline (nl). """
         s = []
         cls = d.__class__
-        md = d.__metadata__
         if d in h:
             s.append('(')
             s.append(cls.__name__)
@@ -299,7 +292,7 @@ class Printer:
         if cls != Object:
             s.append('(')
             if isinstance(d, Facade):
-                s.append(md.facade)
+                s.append(d.__metadata__.facade)
             else:
                 s.append(cls.__name__)
             s.append(')')
@@ -310,7 +303,7 @@ class Printer:
             item = self.unwrap(d, item)
             s.append('\n')
             s.append(self.indent(n+1))
-            if isinstance(item[1], (list,tuple)):            
+            if isinstance(item[1], (list,tuple)):
                 s.append(item[0])
                 s.append('[]')
             else:
@@ -322,7 +315,7 @@ class Printer:
         s.append('}')
         h.pop()
         return ''.join(s)
-    
+
     def print_dictionary(self, d, h, n, nl=False):
         """ print complex using the specified indent (n) and newline (nl). """
         if d in h: return '{}...'
@@ -335,7 +328,7 @@ class Printer:
         for item in d.items():
             s.append('\n')
             s.append(self.indent(n+1))
-            if isinstance(item[1], (list,tuple)):            
+            if isinstance(item[1], (list,tuple)):
                 s.append(tostr(item[0]))
                 s.append('[]')
             else:
@@ -360,10 +353,10 @@ class Printer:
             s.append(',')
         h.pop()
         return ''.join(s)
-    
+
     def unwrap(self, d, item):
         """ translate (unwrap) using an optional wrapper function """
-        nopt = ( lambda x: x )
+        nopt = lambda x: x
         try:
             md = d.__metadata__
             pmd = getattr(md, '__print__', None)
@@ -375,7 +368,7 @@ class Printer:
         except:
             pass
         return item
-    
+
     def exclude(self, d, item):
         """ check metadata for excluded items """
         try:
@@ -384,7 +377,7 @@ class Printer:
             if pmd is None:
                 return False
             excludes = getattr(pmd, 'excludes', [])
-            return ( item[0] in excludes ) 
+            return ( item[0] in excludes )
         except:
             pass
-        return False
\ No newline at end of file
+        return False
diff --git a/suds/transport/__init__.py b/suds/transport/__init__.py
index e1e00d7..b193aea 100644
--- a/suds/transport/__init__.py
+++ b/suds/transport/__init__.py
@@ -1,23 +1,25 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Contains transport interface (classes).
+
 """
 
+from suds import UnicodeMixin
+
 
 class TransportError(Exception):
     def __init__(self, reason, httpcode, fp=None):
@@ -25,106 +27,109 @@ class TransportError(Exception):
         self.httpcode = httpcode
         self.fp = fp
 
-class Request:
+
+class Request(UnicodeMixin):
     """
-    A transport request
-    @ivar url: The url for the request.
+    A transport request.
+
+    @ivar url: The URL for the request.
     @type url: str
     @ivar message: The message to be sent in a POST request.
     @type message: str
-    @ivar headers: The http headers to be used for the request.
+    @ivar headers: The HTTP headers to be used for the request.
     @type headers: dict
+
     """
 
     def __init__(self, url, message=None):
         """
-        @param url: The url for the request.
+        @param url: The URL for the request.
         @type url: str
-        @param message: The (optional) message to be send in the request.
+        @param message: The (optional) message to be sent in the request.
         @type message: str
+
         """
         self.url = url
         self.headers = {}
         self.message = message
-        
-    def __str__(self):
-        s = []
-        s.append('URL:%s' % self.url)
-        s.append('HEADERS: %s' % self.headers)
-        s.append('MESSAGE:')
-        s.append(self.message)
-        return '\n'.join(s)
 
+    def __unicode__(self):
+        return u"""\
+URL: %s
+HEADERS: %s
+MESSAGE:
+%s""" % (self.url, self.headers, self.message)
 
-class Reply:
+
+class Reply(UnicodeMixin):
     """
-    A transport reply
-    @ivar code: The http code returned.
+    A transport reply.
+
+    @ivar code: The HTTP code returned.
     @type code: int
     @ivar message: The message to be sent in a POST request.
     @type message: str
-    @ivar headers: The http headers to be used for the request.
+    @ivar headers: The HTTP headers to be used for the request.
     @type headers: dict
+
     """
 
     def __init__(self, code, headers, message):
         """
-        @param code: The http code returned.
+        @param code: The HTTP code returned.
         @type code: int
-        @param headers: The http returned headers.
+        @param headers: The HTTP returned headers.
         @type headers: dict
         @param message: The (optional) reply message received.
         @type message: str
+
         """
         self.code = code
         self.headers = headers
         self.message = message
-        
-    def __str__(self):
-        s = []
-        s.append('CODE: %s' % self.code)
-        s.append('HEADERS: %s' % self.headers)
-        s.append('MESSAGE:')
-        s.append(self.message)
-        return '\n'.join(s)
+
+    def __unicode__(self):
+        return u"""\
+CODE: %s
+HEADERS: %s
+MESSAGE:
+%s""" % (self.code, self.headers, self.message)
 
 
 class Transport:
-    """
-    The transport I{interface}.
-    """
-    
+    """The transport I{interface}."""
+
     def __init__(self):
-        """
-        Constructor.
-        """
         from suds.transport.options import Options
         self.options = Options()
-        del Options
-    
+
     def open(self, request):
         """
-        Open the url in the specified request.
+        Open the URL in the specified request.
+
         @param request: A transport request.
         @type request: L{Request}
         @return: An input stream.
         @rtype: stream
         @raise TransportError: On all transport errors.
+
         """
         raise Exception('not-implemented')
-    
+
     def send(self, request):
         """
-        Send soap message.  Implementations are expected to handle:
+        Send soap message. Implementations are expected to handle:
             - proxies
-            - I{http} headers
+            - I{HTTP} headers
             - cookies
             - sending message
             - brokering exceptions into L{TransportError}
+
         @param request: A transport request.
         @type request: L{Request}
         @return: The reply
         @rtype: L{Reply}
         @raise TransportError: On all transport errors.
+
         """
         raise Exception('not-implemented')
diff --git a/suds/transport/http.py b/suds/transport/http.py
index 6d85b09..e3d042b 100644
--- a/suds/transport/http.py
+++ b/suds/transport/http.py
@@ -1,41 +1,45 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
 Contains classes for basic HTTP transport implementations.
+
 """
 
-import urllib2 as u2
+from suds.properties import Unskin
+from suds.transport import *
+
 import base64
+from cookielib import CookieJar
+import httplib
 import socket
-from suds.transport import *
-from suds.properties import Unskin
+import sys
+import urllib2
 from urlparse import urlparse
-from cookielib import CookieJar
-from logging import getLogger
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
 class HttpTransport(Transport):
     """
-    HTTP transport using urllib2.  Provided basic http transport
-    that provides for cookies, proxies but no authentication.
+    Basic HTTP transport implemented using using urllib2, that provides for
+    cookies & proxies but no authentication.
+
     """
-    
+
     def __init__(self, **kwargs):
         """
         @param kwargs: Keyword arguments.
@@ -46,40 +50,45 @@ class HttpTransport(Transport):
             - B{timeout} - Set the url open timeout (seconds).
                     - type: I{float}
                     - default: 90
+
         """
         Transport.__init__(self)
         Unskin(self.options).update(kwargs)
         self.cookiejar = CookieJar()
         self.proxy = {}
         self.urlopener = None
-        
+
     def open(self, request):
         try:
-            url = request.url
+            url = self.__get_request_url(request)
             log.debug('opening (%s)', url)
-            u2request = u2.Request(url)
+            u2request = urllib2.Request(url)
             self.proxy = self.options.proxy
             return self.u2open(u2request)
-        except u2.HTTPError, e:
+        except urllib2.HTTPError, e:
             raise TransportError(str(e), e.code, e.fp)
 
     def send(self, request):
         result = None
-        url = request.url
+        url = self.__get_request_url(request)
         msg = request.message
         headers = request.headers
         try:
-            u2request = u2.Request(url, msg, headers)
+            u2request = urllib2.Request(url, msg, headers)
             self.addcookies(u2request)
             self.proxy = self.options.proxy
             request.headers.update(u2request.headers)
             log.debug('sending:\n%s', request)
             fp = self.u2open(u2request)
             self.getcookies(fp, u2request)
-            result = Reply(200, fp.headers.dict, fp.read())
+            if sys.version_info < (3, 0):
+                headers = fp.headers.dict
+            else:
+                headers = fp.headers
+            result = Reply(httplib.OK, headers, fp.read())
             log.debug('received:\n%s', result)
-        except u2.HTTPError, e:
-            if e.code in (202,204):
+        except urllib2.HTTPError, e:
+            if e.code in (httplib.ACCEPTED, httplib.NO_CONTENT):
                 result = None
             else:
                 raise TransportError(e.msg, e.code, e.fp)
@@ -88,70 +97,78 @@ class HttpTransport(Transport):
     def addcookies(self, u2request):
         """
         Add cookies in the cookiejar to the request.
+
         @param u2request: A urllib2 request.
-        @rtype: u2request: urllib2.Requet.
+        @rtype: u2request: urllib2.Request.
+
         """
         self.cookiejar.add_cookie_header(u2request)
-        
+
     def getcookies(self, fp, u2request):
         """
         Add cookies in the request to the cookiejar.
+
         @param u2request: A urllib2 request.
-        @rtype: u2request: urllib2.Requet.
+        @rtype: u2request: urllib2.Request.
+
         """
         self.cookiejar.extract_cookies(fp, u2request)
-        
+
     def u2open(self, u2request):
         """
         Open a connection.
+
         @param u2request: A urllib2 request.
-        @type u2request: urllib2.Requet.
+        @type u2request: urllib2.Request.
         @return: The opened file-like urllib2 object.
         @rtype: fp
+
         """
         tm = self.options.timeout
         url = self.u2opener()
-        if self.u2ver() < 2.6:
+        if (sys.version_info < (3, 0)) and (self.u2ver() < 2.6):
             socket.setdefaulttimeout(tm)
             return url.open(u2request)
-        else:
-            return url.open(u2request, timeout=tm)
-            
+        return url.open(u2request, timeout=tm)
+
     def u2opener(self):
         """
         Create a urllib opener.
+
         @return: An opener.
         @rtype: I{OpenerDirector}
+
         """
         if self.urlopener is None:
-            return u2.build_opener(*self.u2handlers())
-        else:
-            return self.urlopener
-        
+            return urllib2.build_opener(*self.u2handlers())
+        return self.urlopener
+
     def u2handlers(self):
         """
         Get a collection of urllib handlers.
+
         @return: A list of handlers to be installed in the opener.
         @rtype: [Handler,...]
+
         """
         handlers = []
-        handlers.append(u2.ProxyHandler(self.proxy))
+        handlers.append(urllib2.ProxyHandler(self.proxy))
         return handlers
-            
+
     def u2ver(self):
         """
         Get the major/minor version of the urllib2 lib.
+
         @return: The urllib2 version.
         @rtype: float
         """
         try:
-            part = u2.__version__.split('.', 1)
-            n = float('.'.join(part))
-            return n
+            part = urllib2.__version__.split('.', 1)
+            return float('.'.join(part))
         except Exception, e:
             log.exception(e)
             return 0
-        
+
     def __deepcopy__(self, memo={}):
         clone = self.__class__()
         p = Unskin(self.options)
@@ -159,29 +176,79 @@ class HttpTransport(Transport):
         cp.update(p)
         return clone
 
+    @staticmethod
+    def __get_request_url(request):
+        """
+        Returns the given request's URL, properly encoded for use with urllib.
+
+        URLs are allowed to be:
+            under Python 2.x: unicode strings, single-byte strings;
+            under Python 3.x: unicode strings.
+        In any case, they are allowed to contain ASCII characters only. We
+        raise a UnicodeError derived exception if they contain any non-ASCII
+        characters (UnicodeEncodeError or UnicodeDecodeError depending on
+        whether the URL was specified as a unicode or a single-byte string).
+
+        Python 3.x httplib.client implementation must be given a unicode string
+        and not a bytes object and the given string is internally converted to
+        a bytes object using an explicitly specified ASCII encoding.
+
+        Python 2.7 httplib implementation expects the URL passed to it to not
+        be a unicode string. If it is, then passing it to the underlying
+        httplib Request object will cause that object to forcefully convert all
+        of its data to unicode, assuming that data contains ASCII data only and
+        raising a UnicodeDecodeError exception if it does not (caused by simple
+        unicode + string concatenation).
+
+        Python 2.4 httplib implementation does not really care about this as it
+        does not use the internal optimization present in the Python 2.7
+        implementation causing all the requested data to be converted to
+        unicode.
+
+        """
+        url = request.url
+        py2 = sys.version_info < (3, 0)
+        if py2 and isinstance(url, str):
+            encodedURL = url
+            decodedURL = url.decode("ascii")
+        else:
+            # On Python3, calling encode() on a bytes or a bytearray object
+            # raises an AttributeError exception.
+            assert py2 or not isinstance(url, bytes)
+            assert py2 or not isinstance(url, bytearray)
+            decodedURL = url
+            encodedURL = url.encode("ascii")
+        if py2:
+            return encodedURL  # Python 2 urllib - single-byte URL string.
+        return decodedURL  # Python 3 urllib - unicode URL string.
+
 
 class HttpAuthenticated(HttpTransport):
     """
-    Provides basic http authentication for servers that don't follow
-    the specified challenge / response model.  This implementation
-    appends the I{Authorization} http header with base64 encoded
-    credentials on every http request.
+    Provides basic HTTP authentication for servers that do not follow the
+    specified challenge/response model. Appends the I{Authorization} HTTP
+    header with base64 encoded credentials on every HTTP request.
     """
-    
+
     def open(self, request):
         self.addcredentials(request)
         return HttpTransport.open(self, request)
-    
+
     def send(self, request):
         self.addcredentials(request)
         return HttpTransport.send(self, request)
-    
+
     def addcredentials(self, request):
         credentials = self.credentials()
         if not (None in credentials):
-            encoded = base64.encodestring(':'.join(credentials))
-            basic = 'Basic %s' % encoded[:-1]
+            credentials = ':'.join(credentials)
+            if sys.version_info < (3,0):
+                basic = 'Basic %s' % base64.b64encode(credentials)
+            else:
+                encodedBytes = base64.urlsafe_b64encode(credentials.encode())
+                encodedString = encodedBytes.decode()
+                basic = 'Basic %s' % encodedString
             request.headers['Authorization'] = basic
-                 
+
     def credentials(self):
-        return (self.options.username, self.options.password)
\ No newline at end of file
+        return self.options.username, self.options.password
diff --git a/suds/transport/https.py b/suds/transport/https.py
index ed23fd5..cdff55a 100644
--- a/suds/transport/https.py
+++ b/suds/transport/https.py
@@ -1,94 +1,95 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains classes for basic HTTP (authenticated) transport implementations.
+Contains classes for authenticated HTTP transport implementations.
+
 """
 
-import urllib2 as u2
 from suds.transport import *
 from suds.transport.http import HttpTransport
-from logging import getLogger
 
-log = getLogger(__name__)
+import urllib2
 
 
 class HttpAuthenticated(HttpTransport):
     """
-    Provides basic http authentication that follows the RFC-2617 specification.
-    As defined by specifications, credentials are provided to the server
-    upon request (HTTP/1.0 401 Authorization Required) by the server only.
+    Provides basic HTTP authentication that follows the RFC-2617 specification.
+
+    As defined by specifications, credentials are provided to the server upon
+    request (HTTP/1.0 401 Authorization Required) by the server only.
+
     @ivar pm: The password manager.
     @ivar handler: The authentication handler.
+
     """
-    
+
     def __init__(self, **kwargs):
         """
         @param kwargs: Keyword arguments.
-            - B{proxy} - An http proxy to be specified on requests.
+            - B{proxy} - An HTTP proxy to be specified on requests.
                  The proxy is defined as {protocol:proxy,}
                     - type: I{dict}
                     - default: {}
-            - B{timeout} - Set the url open timeout (seconds).
+            - B{timeout} - Set the URL open timeout (seconds).
                     - type: I{float}
                     - default: 90
-            - B{username} - The username used for http authentication.
+            - B{username} - The username used for HTTP authentication.
                     - type: I{str}
                     - default: None
-            - B{password} - The password used for http authentication.
+            - B{password} - The password used for HTTP authentication.
                     - type: I{str}
                     - default: None
+
         """
         HttpTransport.__init__(self, **kwargs)
-        self.pm = u2.HTTPPasswordMgrWithDefaultRealm()
-        
+        self.pm = urllib2.HTTPPasswordMgrWithDefaultRealm()
+
     def open(self, request):
         self.addcredentials(request)
-        return  HttpTransport.open(self, request)
-    
+        return HttpTransport.open(self, request)
+
     def send(self, request):
         self.addcredentials(request)
-        return  HttpTransport.send(self, request)
-    
+        return HttpTransport.send(self, request)
+
     def addcredentials(self, request):
         credentials = self.credentials()
-        if not (None in credentials):
+        if None not in credentials:
             u = credentials[0]
             p = credentials[1]
             self.pm.add_password(None, request.url, u, p)
-    
+
     def credentials(self):
-        return (self.options.username, self.options.password)
-    
+        return self.options.username, self.options.password
+
     def u2handlers(self):
-            handlers = HttpTransport.u2handlers(self)
-            handlers.append(u2.HTTPBasicAuthHandler(self.pm))
-            return handlers
-    
-    
+        handlers = HttpTransport.u2handlers(self)
+        handlers.append(urllib2.HTTPBasicAuthHandler(self.pm))
+        return handlers
+
+
 class WindowsHttpAuthenticated(HttpAuthenticated):
     """
-    Provides Windows (NTLM) http authentication.
-    @ivar pm: The password manager.
-    @ivar handler: The authentication handler.
+    Provides Windows (NTLM) based HTTP authentication.
+
     @author: Christopher Bess
+
     """
-        
+
     def u2handlers(self):
-        # try to import ntlm support  
         try:
             from ntlm import HTTPNtlmAuthHandler
         except ImportError:
diff --git a/suds/transport/options.py b/suds/transport/options.py
index 8b0d194..f6a071e 100644
--- a/suds/transport/options.py
+++ b/suds/transport/options.py
@@ -1,50 +1,52 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-Contains classes for transport options.
+Classes modeling transport options.
+
 """
 
 
 from suds.transport import *
 from suds.properties import *
 
-   
+
 class Options(Skin):
     """
     Options:
-        - B{proxy} - An http proxy to be specified on requests.
-             The proxy is defined as {protocol:proxy,}
+        - B{proxy} - An HTTP proxy to be specified on requests, defined as
+            {protocol:proxy, ...}.
                 - type: I{dict}
                 - default: {}
-        - B{timeout} - Set the url open timeout (seconds).
+        - B{timeout} - Set the URL open timeout (seconds).
                 - type: I{float}
                 - default: 90
         - B{headers} - Extra HTTP headers.
                 - type: I{dict}
-                    - I{str} B{http} - The I{http} protocol proxy URL.
-                    - I{str} B{https} - The I{https} protocol proxy URL.
+                    - I{str} B{http} - The I{HTTP} protocol proxy URL.
+                    - I{str} B{https} - The I{HTTPS} protocol proxy URL.
                 - default: {}
-        - B{username} - The username used for http authentication.
+        - B{username} - The username used for HTTP authentication.
                 - type: I{str}
                 - default: None
-        - B{password} - The password used for http authentication.
+        - B{password} - The password used for HTTP authentication.
                 - type: I{str}
                 - default: None
-    """    
+
+    """
+
     def __init__(self, **kwargs):
         domain = __name__
         definitions = [
@@ -52,6 +54,5 @@ class Options(Skin):
             Definition('timeout', (int,float), 90),
             Definition('headers', dict, {}),
             Definition('username', basestring, None),
-            Definition('password', basestring, None),
-        ]
-        Skin.__init__(self, domain, definitions, kwargs)
\ No newline at end of file
+            Definition('password', basestring, None)]
+        Skin.__init__(self, domain, definitions, kwargs)
diff --git a/suds/umx/__init__.py b/suds/umx/__init__.py
index 9d06b40..ca65cad 100644
--- a/suds/umx/__init__.py
+++ b/suds/umx/__init__.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -32,7 +32,7 @@ class Content(Object):
     @ivar text: The (optional) content (xml) text.
     @type text: basestring
     """
-    
+
     extensions = []
 
     def __init__(self, node, **kwargs):
@@ -53,4 +53,4 @@ class Content(Object):
                     'Content has no attribute %s' % name
         else:
             v = self.__dict__[name]
-        return v
\ No newline at end of file
+        return v
diff --git a/suds/umx/attrlist.py b/suds/umx/attrlist.py
index 3694327..df8da0b 100644
--- a/suds/umx/attrlist.py
+++ b/suds/umx/attrlist.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -37,7 +37,7 @@ class AttrList:
         @type attributes: list
         """
         self.raw = attributes
-        
+
     def real(self):
         """
         Get list of I{real} attributes which exclude xs and xml attributes.
@@ -47,18 +47,18 @@ class AttrList:
         for a in self.raw:
             if self.skip(a): continue
             yield a
-            
+
     def rlen(self):
         """
         Get the number of I{real} attributes which exclude xs and xml attributes.
-        @return: A count of I{real} attributes. 
+        @return: A count of I{real} attributes.
         @rtype: L{int}
         """
         n = 0
         for a in self.real():
             n += 1
         return n
-            
+
     def lang(self):
         """
         Get list of I{filtered} attributes which exclude xs.
diff --git a/suds/umx/basic.py b/suds/umx/basic.py
index cdc1e66..888a212 100644
--- a/suds/umx/basic.py
+++ b/suds/umx/basic.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -28,7 +28,7 @@ class Basic(Core):
     """
     A object builder (unmarshaller).
     """
-        
+
     def process(self, node):
         """
         Process an object graph representation of the xml I{node}.
@@ -38,4 +38,4 @@ class Basic(Core):
         @rtype: L{Object}
         """
         content = Content(node)
-        return Core.process(self, content)
\ No newline at end of file
+        return Core.process(self, content)
diff --git a/suds/umx/core.py b/suds/umx/core.py
index 07d33c4..4db8eaa 100644
--- a/suds/umx/core.py
+++ b/suds/umx/core.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,7 +18,6 @@
 Provides base classes for XML->object I{unmarshalling}.
 """
 
-from logging import getLogger
 from suds import *
 from suds.umx import *
 from suds.umx.attrlist import AttrList
@@ -26,16 +25,15 @@ from suds.sax.text import Text
 from suds.sudsobject import Factory, merge
 
 
-log = getLogger(__name__)
+reserved = {'class':'cls', 'def':'dfn'}
 
-reserved = { 'class':'cls', 'def':'dfn', }
 
 class Core:
     """
     The abstract XML I{node} unmarshaller.  This class provides the
     I{core} unmarshalling functionality.
     """
-        
+
     def process(self, content):
         """
         Process an object graph representation of the xml I{node}.
@@ -46,7 +44,7 @@ class Core:
         """
         self.reset()
         return self.append(content)
-    
+
     def append(self, content):
         """
         Process the specified node and convert the XML document into
@@ -64,14 +62,14 @@ class Core:
         self.append_text(content)
         self.end(content)
         return self.postprocess(content)
-            
+
     def postprocess(self, content):
         """
         Perform final processing of the resulting data structure as follows:
           - Mixed values (children and text) will have a result of the I{content.node}.
           - Simi-simple values (attributes, no-children and text) will have a result of a
              property object.
-          - Simple values (no-attributes, no-children with text nodes) will have a string 
+          - Simple values (no-attributes, no-children with text nodes) will have a string
              result equal to the value of the content.node.getText().
         @param content: The current content being unmarshalled.
         @type content: L{Content}
@@ -101,7 +99,7 @@ class Core:
             return Text(content.text, lang=lang)
         else:
             return content.text
-    
+
     def append_attributes(self, content):
         """
         Append attribute nodes into L{Content.data}.
@@ -114,7 +112,7 @@ class Core:
             name = attr.name
             value = attr.value
             self.append_attribute(name, value, content)
-            
+
     def append_attribute(self, name, value, content):
         """
         Append an attribute name/value into L{Content.data}.
@@ -128,7 +126,7 @@ class Core:
         key = name
         key = '_%s' % reserved.get(key, key)
         setattr(content.data, key, value)
-            
+
     def append_children(self, content):
         """
         Append child nodes into L{Content.data}
@@ -146,14 +144,14 @@ class Core:
                 else:
                     setattr(content.data, key, [v, cval])
                 continue
-            if self.unbounded(cont):
+            if self.multi_occurrence(cont):
                 if cval is None:
                     setattr(content.data, key, [])
                 else:
                     setattr(content.data, key, [cval,])
             else:
                 setattr(content.data, key, cval)
-    
+
     def append_text(self, content):
         """
         Append text nodes into L{Content.data}
@@ -162,7 +160,7 @@ class Core:
         """
         if content.node.hasText():
             content.text = content.node.getText()
-        
+
     def reset(self):
         pass
 
@@ -176,7 +174,7 @@ class Core:
         @rtype: L{Object}
         """
         content.data = Factory.object(content.node.name)
-    
+
     def end(self, content):
         """
         Processing on I{node} has ended.
@@ -184,27 +182,27 @@ class Core:
         @type content: L{Content}
         """
         pass
-    
-    def bounded(self, content):
+
+    def single_occurrence(self, content):
         """
-        Get whether the content is bounded (not a list).
+        Get whether the content has at most a single occurrence (not a list).
         @param content: The current content being unmarshalled.
         @type content: L{Content}
-        @return: True if bounded, else False
+        @return: True if content has at most a single occurrence, else False.
         @rtype: boolean
         '"""
-        return ( not self.unbounded(content) )
-    
-    def unbounded(self, content):
+        return not self.multi_occurrence(content)
+
+    def multi_occurrence(self, content):
         """
-        Get whether the object is unbounded (a list).
+        Get whether the content has more than one occurrence (a list).
         @param content: The current content being unmarshalled.
         @type content: L{Content}
-        @return: True if unbounded, else False
+        @return: True if content has more than one occurrence, else False.
         @rtype: boolean
         '"""
         return False
-    
+
     def nillable(self, content):
         """
         Get whether the object is nillable.
@@ -213,4 +211,4 @@ class Core:
         @return: True if nillable, else False
         @rtype: boolean
         '"""
-        return False
\ No newline at end of file
+        return False
diff --git a/suds/umx/encoded.py b/suds/umx/encoded.py
index afe7374..bb454e1 100644
--- a/suds/umx/encoded.py
+++ b/suds/umx/encoded.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,13 +18,11 @@
 Provides soap encoded unmarshaller classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.umx import *
 from suds.umx.typed import Typed
-from suds.sax import splitPrefix, Namespace
+from suds.sax import Namespace
 
-log = getLogger(__name__)
 
 #
 # Add encoded extensions
@@ -45,7 +43,7 @@ class Encoded(Typed):
         #
         self.setaty(content)
         Typed.start(self, content)
-    
+
     def end(self, content):
         #
         # Squash soap encoded arrays into python lists.  This is
@@ -56,7 +54,7 @@ class Encoded(Typed):
         if aty is not None:
             self.promote(content)
         return Typed.end(self, content)
-    
+
     def postprocess(self, content):
         #
         # Ensure proper rendering of empty arrays.
@@ -65,7 +63,7 @@ class Encoded(Typed):
             return Typed.postprocess(self, content)
         else:
             return content.data
-    
+
     def setaty(self, content):
         """
         Grab the (aty) soap-enc:arrayType and attach it to the
@@ -87,11 +85,11 @@ class Encoded(Typed):
             else:
                 pass # (2) dimensional array
         return self
-    
+
     def applyaty(self, content, xty):
         """
         Apply the type referenced in the I{arrayType} to the content
-        (child nodes) of the array.  Each element (node) in the array 
+        (child nodes) of the array.  Each element (node) in the array
         that does not have an explicit xsi:type attribute is given one
         based on the I{arrayType}.
         @param content: An array content.
@@ -115,7 +113,7 @@ class Encoded(Typed):
     def promote(self, content):
         """
         Promote (replace) the content.data with the first attribute
-        of the current content.data that is a I{list}.  Note: the 
+        of the current content.data that is a I{list}.  Note: the
         content.data may be empty or contain only _x attributes.
         In either case, the content.data is assigned an empty list.
         @param content: An array content.
@@ -125,4 +123,4 @@ class Encoded(Typed):
             if isinstance(v, list):
                 content.data = v
                 return
-        content.data = []
\ No newline at end of file
+        content.data = []
diff --git a/suds/umx/typed.py b/suds/umx/typed.py
index f272a25..7ab8329 100644
--- a/suds/umx/typed.py
+++ b/suds/umx/typed.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,13 +18,13 @@
 Provides typed unmarshaller classes.
 """
 
-from logging import getLogger
 from suds import *
 from suds.umx import *
 from suds.umx.core import Core
 from suds.resolver import NodeResolver, Frame
 from suds.sudsobject import Factory
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -43,14 +43,14 @@ class Typed(Core):
     @ivar resolver: A schema type resolver.
     @type resolver: L{NodeResolver}
     """
-    
+
     def __init__(self, schema):
         """
         @param schema: A schema object.
         @type schema: L{xsd.schema.Schema}
         """
         self.resolver = NodeResolver(schema)
-        
+
     def process(self, node, type):
         """
         Process an object graph representation of the xml L{node}.
@@ -68,7 +68,7 @@ class Typed(Core):
     def reset(self):
         log.debug('reset')
         self.resolver.reset()
-    
+
     def start(self, content):
         #
         # Resolve to the schema type; build an object and setup metadata.
@@ -91,18 +91,18 @@ class Typed(Core):
         content.data = Factory.object(cls_name)
         md = content.data.__metadata__
         md.sxtype = real
-        
+
     def end(self, content):
         self.resolver.pop()
-        
-    def unbounded(self, content):
-        return content.type.unbounded()
-    
+
+    def multi_occurrence(self, content):
+        return content.type.multi_occurrence()
+
     def nillable(self, content):
         resolved = content.type.resolve()
         return ( content.type.nillable or \
             (resolved.builtin() and resolved.nillable ) )
-    
+
     def append_attribute(self, name, value, content):
         """
         Append an attribute name/value into L{Content.data}.
@@ -119,7 +119,7 @@ class Typed(Core):
         else:
             value = self.translated(value, type)
         Core.append_attribute(self, name, value, content)
-    
+
     def append_text(self, content):
         """
         Append text nodes into L{Content.data}
@@ -131,11 +131,10 @@ class Typed(Core):
         Core.append_text(self, content)
         known = self.resolver.top().resolved
         content.text = self.translated(content.text, known)
-            
+
     def translated(self, value, type):
         """ translate using the schema type """
         if value is not None:
             resolved = type.resolve()
             return resolved.translate(value)
-        else:
-            return value
\ No newline at end of file
+        return value
diff --git a/suds/bindings/__init__.py b/suds/version.py
similarity index 69%
copy from suds/bindings/__init__.py
copy to suds/version.py
index 5471eba..c2367b2 100644
--- a/suds/bindings/__init__.py
+++ b/suds/version.py
@@ -1,20 +1,26 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-"""
-Provides modules containing classes to support Web Services (SOAP)
-bindings.
-"""
\ No newline at end of file
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""
+Module containing the library's version information.
+
+  This version information has been extracted into a separate file so it can be
+read from the setup.py script without having to import the suds package itself.
+See the setup.py script for more detailed information.
+
+"""
+
+__version__ = "0.6"
+__build__ = ""
diff --git a/suds/wsdl.py b/suds/wsdl.py
index 8bba88f..987dbc3 100644
--- a/suds/wsdl.py
+++ b/suds/wsdl.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -16,13 +16,11 @@
 
 """
 The I{wsdl} module provides an objectification of the WSDL.
-The primary class is I{Definitions} as it represends the root element
+The primary class is I{Definitions} as it represents the root element
 found in the document.
 """
 
-from logging import getLogger
 from suds import *
-from suds.sax import splitPrefix
 from suds.sax.element import Element
 from suds.bindings.document import Document
 from suds.bindings.rpc import RPC, Encoded
@@ -30,12 +28,16 @@ from suds.xsd import qualify, Namespace
 from suds.xsd.schema import Schema, SchemaCollection
 from suds.xsd.query import ElementQuery
 from suds.sudsobject import Object, Facade, Metadata
-from suds.reader import DocumentReader, DefinitionsReader
+from suds.reader import DocumentReader
+
+import re
+import soaparray
 from urlparse import urljoin
-import re, soaparray
 
+from logging import getLogger
 log = getLogger(__name__)
 
+
 wsdlns = (None, "http://schemas.xmlsoap.org/wsdl/")
 soapns = (None, 'http://schemas.xmlsoap.org/wsdl/soap/')
 soap12ns = (None, 'http://schemas.xmlsoap.org/wsdl/soap12/')
@@ -43,17 +45,15 @@ soap12ns = (None, 'http://schemas.xmlsoap.org/wsdl/soap12/')
 
 class WObject(Object):
     """
-    Base object for wsdl types.
+    Base object for WSDL types.
     @ivar root: The XML I{root} element.
     @type root: L{Element}
     """
-    
-    def __init__(self, root, definitions=None):
+
+    def __init__(self, root):
         """
         @param root: An XML root element.
         @type root: L{Element}
-        @param definitions: A definitions object.
-        @type definitions: L{Definitions}
         """
         Object.__init__(self)
         self.root = root
@@ -61,7 +61,7 @@ class WObject(Object):
         pmd.excludes = ['root']
         pmd.wrappers = dict(qname=repr)
         self.__metadata__.__print__ = pmd
-        
+
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.
@@ -70,7 +70,7 @@ class WObject(Object):
         """
         pass
 
-        
+
 class NamedObject(WObject):
     """
     A B{named} WSDL object.
@@ -87,7 +87,7 @@ class NamedObject(WObject):
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         """
-        WObject.__init__(self, root, definitions)
+        WObject.__init__(self, root)
         self.name = root.get('name')
         self.qname = (self.name, definitions.tns[1])
         pmd = self.__metadata__.__print__
@@ -96,8 +96,8 @@ class NamedObject(WObject):
 
 class Definitions(WObject):
     """
-    Represents the I{root} container of the WSDL objects as defined
-    by <wsdl:definitions/>
+    I{root} container for all the WSDL objects as defined by
+    <wsdl:definitions/>
     @ivar id: The object id.
     @type id: str
     @ivar options: An options dictionary.
@@ -121,7 +121,7 @@ class Definitions(WObject):
     @ivar service: The service object.
     @type service: L{Service}
     """
-    
+
     Tag = 'definitions'
 
     def __init__(self, url, options):
@@ -131,7 +131,7 @@ class Definitions(WObject):
         @param options: An options dictionary.
         @type options: L{options.Options}
         """
-        log.debug('reading wsdl at: %s ...', url)
+        log.debug('reading WSDL at: %s ...', url)
         reader = DocumentReader(options)
         d = reader.open(url)
         root = d.root()
@@ -160,8 +160,8 @@ class Definitions(WObject):
         self.set_wrapped()
         for s in self.services:
             self.add_methods(s)
-        log.debug("wsdl at '%s' loaded:\n%s", url, self)
-        
+        log.debug("WSDL at '%s' loaded:\n%s", url, self)
+
     def mktns(self, root):
         """ Get/create the target namespace """
         tns = root.get('targetNamespace')
@@ -170,7 +170,7 @@ class Definitions(WObject):
             log.debug('warning: tns (%s), not mapped to prefix', tns)
             prefix = 'tns'
         return (prefix, tns)
-        
+
     def add_children(self, root):
         """ Add child objects using the factory """
         for c in root.getChildren(ns=wsdlns):
@@ -195,17 +195,17 @@ class Definitions(WObject):
             if isinstance(child, Service):
                 self.services.append(child)
                 continue
-                
+
     def open_imports(self):
         """ Import the I{imported} WSDLs. """
         for imp in self.imports:
             imp.load(self)
-                
+
     def resolve(self):
         """ Tell all children to resolve themselves """
         for c in self.children:
             c.resolve(self)
-                
+
     def build_schema(self):
         """ Process L{Types} objects and create the schema collection """
         container = SchemaCollection(self)
@@ -213,7 +213,7 @@ class Definitions(WObject):
             for root in t.contents():
                 schema = Schema(root, self.url, self.options, container)
                 container.add(schema)
-        if not len(container): # empty
+        if not len(container):
             root = Element.buildPath(self.root, 'types/schema')
             schema = Schema(root, self.url, self.options, container)
             container.add(schema)
@@ -221,7 +221,7 @@ class Definitions(WObject):
         for s in [t.schema() for t in self.types if t.imported()]:
             self.schema.merge(s)
         return self.schema
-                
+
     def add_methods(self, service):
         """ Build method view for service """
         bindings = {
@@ -246,13 +246,15 @@ class Definitions(WObject):
                 m.binding.output = bindings.get(key)
                 op = ptype.operation(name)
                 p.methods[name] = m
-                
+
     def set_wrapped(self):
         """ set (wrapped|bare) flag on messages """
         for b in self.bindings.values():
             for op in b.operations.values():
                 for body in (op.soap.input.body, op.soap.output.body):
                     body.wrapped = False
+                    if not self.options.unwrap:
+                        continue
                     if len(body.parts) != 1:
                         continue
                     for p in body.parts:
@@ -266,7 +268,7 @@ class Definitions(WObject):
                         if resolved.builtin():
                             continue
                         body.wrapped = True
-                        
+
     def __getstate__(self):
         nopickle = ('options',)
         state = self.__dict__.copy()
@@ -274,7 +276,7 @@ class Definitions(WObject):
             if k in state:
                 del state[k]
         return state
-    
+
     def __repr__(self):
         return 'Definitions (id=%s)' % self.id
 
@@ -289,7 +291,7 @@ class Import(WObject):
     @ivar imported: The imported object.
     @type imported: L{Definitions}
     """
-    
+
     def __init__(self, root, definitions):
         """
         @param root: An XML root element.
@@ -297,13 +299,13 @@ class Import(WObject):
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         """
-        WObject.__init__(self, root, definitions)
+        WObject.__init__(self, root)
         self.location = root.get('location')
         self.ns = root.get('namespace')
         self.imported = None
         pmd = self.__metadata__.__print__
         pmd.wrappers['imported'] = repr
-        
+
     def load(self, definitions):
         """ Load the object by opening the URL """
         url = self.location
@@ -319,40 +321,36 @@ class Import(WObject):
             self.import_schema(definitions, d)
             return
         raise Exception('document at "%s" is unknown' % url)
-    
+
     def import_definitions(self, definitions, d):
-        """ import/merge wsdl definitions """
+        """ import/merge WSDL definitions """
         definitions.types += d.types
         definitions.messages.update(d.messages)
         definitions.port_types.update(d.port_types)
         definitions.bindings.update(d.bindings)
         self.imported = d
         log.debug('imported (WSDL):\n%s', d)
-        
+
     def import_schema(self, definitions, d):
         """ import schema as <types/> content """
         if not len(definitions.types):
-            types = Types.create(definitions)
+            root = Element('types', ns=wsdlns)
+            definitions.root.insert(root)
+            types = Types(root, definitions)
             definitions.types.append(types)
         else:
             types = definitions.types[-1]
         types.root.append(d.root)
         log.debug('imported (XSD):\n%s', d.root)
-   
+
     def __gt__(self, other):
         return False
-        
+
 
 class Types(WObject):
     """
     Represents <types><schema/></types>.
     """
-    
-    @classmethod
-    def create(cls, definitions):
-        root = Element('types', ns=wsdlns)
-        definitions.root.insert(root)
-        return Types(root, definitions)
 
     def __init__(self, root, definitions):
         """
@@ -361,24 +359,24 @@ class Types(WObject):
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         """
-        WObject.__init__(self, root, definitions)
+        WObject.__init__(self, root)
         self.definitions = definitions
-        
+
     def contents(self):
         return self.root.getChildren('schema', Namespace.xsdns)
-    
+
     def schema(self):
         return self.definitions.schema
-    
+
     def local(self):
         return ( self.definitions.schema is None )
-    
+
     def imported(self):
         return ( not self.local() )
-        
+
     def __gt__(self, other):
         return isinstance(other, Import)
-    
+
 
 class Part(NamedObject):
     """
@@ -405,14 +403,14 @@ class Part(NamedObject):
         tns = definitions.tns
         self.element = self.__getref('element', tns)
         self.type = self.__getref('type', tns)
-        
+
     def __getref(self, a, tns):
         """ Get the qualified value of attribute named 'a'."""
         s = self.root.get(a)
         if s is None:
             return s
         else:
-            return qualify(s, self.root, tns)  
+            return qualify(s, self.root, tns)
 
 
 class Message(NamedObject):
@@ -434,11 +432,11 @@ class Message(NamedObject):
         for p in root.getChildren('part'):
             part = Part(p, definitions)
             self.parts.append(part)
-            
+
     def __gt__(self, other):
         return isinstance(other, (Import, Types))
-    
-    
+
+
 class PortType(NamedObject):
     """
     Represents <portType/>.
@@ -477,7 +475,7 @@ class PortType(NamedObject):
                 faults.append(f)
             op.faults = faults
             self.operations[op.name] = op
-            
+
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.
@@ -509,7 +507,7 @@ class PortType(NamedObject):
                 if msg is None:
                     raise Exception, "msg '%s', not-found" % f.message
                 f.message = msg
-                
+
     def operation(self, name):
         """
         Shortcut used to get a contained operation by name.
@@ -523,7 +521,7 @@ class PortType(NamedObject):
             return self.operations[name]
         except Exception, e:
             raise MethodNotFound(name)
-                
+
     def __gt__(self, other):
         return isinstance(other, (Import, Types, Message))
 
@@ -548,21 +546,21 @@ class Binding(NamedObject):
         sr = self.soaproot()
         if sr is None:
             self.soap = None
-            log.debug('binding: "%s" not a soap binding', self.name)
+            log.debug('binding: "%s" not a SOAP binding', self.name)
             return
         soap = Facade('soap')
         self.soap = soap
         self.soap.style = sr.get('style', default='document')
         self.add_operations(self.root, definitions)
-        
+
     def soaproot(self):
         """ get the soap:binding """
         for ns in (soapns, soap12ns):
-            sr =  self.root.getChild('binding', ns=ns)
+            sr = self.root.getChild('binding', ns=ns)
             if sr is not None:
                 return sr
         return None
-        
+
     def add_operations(self, root, definitions):
         """ Add <operation/> children """
         dsop = Element('operation', ns=soapns)
@@ -606,7 +604,7 @@ class Binding(NamedObject):
                 faults.append(f)
             soap.faults = faults
             self.operations[op.name] = op
-            
+
     def body(self, definitions, body, root):
         """ add the input/output body properties """
         if root is None:
@@ -626,7 +624,7 @@ class Binding(NamedObject):
         else:
             prefix = root.findPrefix(ns, 'b0')
             body.namespace = (prefix, ns)
-            
+
     def header(self, definitions, parent, root):
         """ add the input/output header properties """
         if root is None:
@@ -646,11 +644,11 @@ class Binding(NamedObject):
         part = root.get('part')
         if part is not None:
             header.part = part
-            
+
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.  This includes
-        cross-linking information (from) the portType (to) the I{soap}
+        cross-linking information (from) the portType (to) the I{SOAP}
         protocol information on the binding for each operation.
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
@@ -660,7 +658,7 @@ class Binding(NamedObject):
             self.resolvesoapbody(definitions, op)
             self.resolveheaders(definitions, op)
             self.resolvefaults(definitions, op)
-        
+
     def resolveport(self, definitions):
         """
         Resolve port_type reference.
@@ -673,10 +671,10 @@ class Binding(NamedObject):
             raise Exception("portType '%s', not-found" % self.type)
         else:
             self.type = port_type
-            
+
     def resolvesoapbody(self, definitions, op):
         """
-        Resolve soap body I{message} parts by 
+        Resolve SOAP body I{message} parts by
         cross-referencing with operation defined in port type.
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
@@ -706,10 +704,10 @@ class Binding(NamedObject):
             soap.output.body.parts = pts
         else:
             soap.output.body.parts = ptop.output.parts
-            
+
     def resolveheaders(self, definitions, op):
         """
-        Resolve soap header I{message} references.
+        Resolve SOAP header I{message} references.
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @param op: An I{operation} object.
@@ -731,11 +729,11 @@ class Binding(NamedObject):
             if pn == header.part:
                 raise Exception, \
                     "message '%s' has not part named '%s'" % (ref, pn)
-                        
+
     def resolvefaults(self, definitions, op):
         """
-        Resolve soap fault I{message} references by
-        cross-referencing with operation defined in port type.
+        Resolve SOAP fault I{message} references by
+        cross-referencing with operations defined in the port type.
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @param op: An I{operation} object.
@@ -755,7 +753,7 @@ class Binding(NamedObject):
                 continue
             raise Exception, \
                 "fault '%s' not defined in portType '%s'" % (fault.name, self.type.name)
-            
+
     def operation(self, name):
         """
         Shortcut used to get a contained operation by name.
@@ -769,7 +767,7 @@ class Binding(NamedObject):
             return self.operations[name]
         except:
             raise MethodNotFound(name)
-            
+
     def __gt__(self, other):
         return ( not isinstance(other, Service) )
 
@@ -781,10 +779,10 @@ class Port(NamedObject):
     @type service: L{Service}
     @ivar binding: A binding name.
     @type binding: str
-    @ivar location: The service location (url).
+    @ivar location: The service location (URL).
     @type location: str
     """
-    
+
     def __init__(self, root, definitions, service):
         """
         @param root: An XML root element.
@@ -798,12 +796,9 @@ class Port(NamedObject):
         self.__service = service
         self.binding = root.get('binding')
         address = root.getChild('address')
-        if address is None:
-            self.location = None
-        else:
-            self.location = address.get('location').encode('utf-8')
+        self.location = address is not None and address.get('location')
         self.methods = {}
-        
+
     def method(self, name):
         """
         Get a method defined in this portType by name.
@@ -813,7 +808,7 @@ class Port(NamedObject):
         @rtype: I{Method}
         """
         return self.methods.get(name)
-        
+
 
 class Service(NamedObject):
     """
@@ -823,7 +818,7 @@ class Service(NamedObject):
     @ivar methods: The contained methods for all ports.
     @type methods: [Method,..]
     """
-    
+
     def __init__(self, root, definitions):
         """
         @param root: An XML root element.
@@ -836,25 +831,25 @@ class Service(NamedObject):
         for p in root.getChildren('port'):
             port = Port(p, definitions, self)
             self.ports.append(port)
-            
+
     def port(self, name):
         """
         Locate a port by name.
         @param name: A port name.
         @type name: str
         @return: The port object.
-        @rtype: L{Port} 
+        @rtype: L{Port}
         """
         for p in self.ports:
             if p.name == name:
                 return p
         return None
-    
+
     def setlocation(self, url, names=None):
         """
-        Override the invocation location (url) for service method.
-        @param url: A url location.
-        @type url: A url.
+        Override the invocation location (URL) for service method.
+        @param url: A URL location.
+        @type url: A URL.
         @param names:  A list of method names.  None=ALL
         @type names: [str,..]
         """
@@ -862,11 +857,11 @@ class Service(NamedObject):
             for m in p.methods.values():
                 if names is None or m.name in names:
                     m.location = url
-        
+
     def resolve(self, definitions):
         """
         Resolve named references to other WSDL objects.
-        Ports without soap bindings are discarded.
+        Ports without SOAP bindings are discarded.
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         """
@@ -877,12 +872,12 @@ class Service(NamedObject):
             if binding is None:
                 raise Exception("binding '%s', not-found" % p.binding)
             if binding.soap is None:
-                log.debug('binding "%s" - not a soap, discarded', binding.name)
+                log.debug('binding "%s" - not a SOAP binding, discarded', binding.name)
                 continue
             p.binding = binding
             filtered.append(p)
         self.ports = filtered
-        
+
     def __gt__(self, other):
         return True
 
@@ -896,14 +891,14 @@ class Factory:
 
     tags =\
     {
-        'import' : Import, 
-        'types' : Types, 
-        'message' : Message, 
+        'import' : Import,
+        'types' : Types,
+        'message' : Message,
         'portType' : PortType,
         'binding' : Binding,
         'service' : Service,
     }
-    
+
     @classmethod
     def create(cls, root, definitions):
         """
@@ -913,7 +908,7 @@ class Factory:
         @param definitions: A definitions object.
         @type definitions: L{Definitions}
         @return: The created object.
-        @rtype: L{WObject} 
+        @rtype: L{WObject}
         """
         fn = cls.tags.get(root.name)
         if fn is not None:
diff --git a/suds/wsse.py b/suds/wsse.py
index 2a697c1..c2f7f52 100644
--- a/suds/wsse.py
+++ b/suds/wsse.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -22,7 +22,7 @@ from logging import getLogger
 from suds import *
 from suds.sudsobject import Object
 from suds.sax.element import Element
-from suds.sax.date import UTC
+from suds.sax.date import DateTime, UtcTimezone
 from datetime import datetime, timedelta
 
 try:
@@ -36,7 +36,7 @@ dsns = \
     ('ds',
      'http://www.w3.org/2000/09/xmldsig#')
 wssens = \
-    ('wsse', 
+    ('wsse',
      'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd')
 wsuns = \
     ('wsu',
@@ -58,7 +58,7 @@ class Security(Object):
     @ivar keys: A list of encryption keys.
     @type keys: TBD
     """
-    
+
     def __init__(self):
         """ """
         Object.__init__(self)
@@ -67,7 +67,7 @@ class Security(Object):
         self.signatures = []
         self.references = []
         self.keys = []
-        
+
     def xml(self):
         """
         Get xml representation of the object.
@@ -83,20 +83,20 @@ class Security(Object):
 
 class Token(Object):
     """ I{Abstract} security token. """
-    
+
     @classmethod
     def now(cls):
         return datetime.now()
-    
+
     @classmethod
     def utc(cls):
-        return datetime.utcnow()
-    
+        return datetime.utcnow().replace(tzinfo=UtcTimezone())
+
     @classmethod
     def sysdate(cls):
-        utc = UTC()
+        utc = DateTime(self.utc())
         return str(utc)
-    
+
     def __init__(self):
             Object.__init__(self)
 
@@ -108,7 +108,7 @@ class UsernameToken(Token):
     @type username: str
     @ivar password: A password.
     @type password: str
-    @ivar nonce: A set of bytes to prevent reply attacks.
+    @ivar nonce: A set of bytes to prevent replay attacks.
     @type nonce: str
     @ivar created: The token created.
     @type created: L{datetime}
@@ -126,11 +126,11 @@ class UsernameToken(Token):
         self.password = password
         self.nonce = None
         self.created = None
-        
+
     def setnonce(self, text=None):
         """
-        Set I{nonce} which is arbitraty set of bytes to prevent
-        reply attacks.
+        Set I{nonce} which is an arbitrary set of bytes to prevent replay
+        attacks.
         @param text: The nonce text value.
             Generated when I{None}.
         @type text: str
@@ -145,7 +145,7 @@ class UsernameToken(Token):
             self.nonce = m.hexdigest()
         else:
             self.nonce = text
-        
+
     def setcreated(self, dt=None):
         """
         Set I{created}.
@@ -157,8 +157,8 @@ class UsernameToken(Token):
             self.created = Token.utc()
         else:
             self.created = dt
-        
-        
+
+
     def xml(self):
         """
         Get xml representation of the object.
@@ -178,7 +178,7 @@ class UsernameToken(Token):
             root.append(n)
         if self.created is not None:
             n = Element('Created', ns=wsuns)
-            n.setText(str(UTC(self.created)))
+            n.setText(str(DateTime(self.created)))
             root.append(n)
         return root
 
@@ -200,13 +200,13 @@ class Timestamp(Token):
         Token.__init__(self)
         self.created = Token.utc()
         self.expires = self.created + timedelta(seconds=validity)
-        
+
     def xml(self):
         root = Element("Timestamp", ns=wsuns)
         created = Element('Created', ns=wsuns)
-        created.setText(str(UTC(self.created)))
+        created.setText(str(DateTime(self.created)))
         expires = Element('Expires', ns=wsuns)
-        expires.setText(str(UTC(self.expires)))
+        expires.setText(str(DateTime(self.expires)))
         root.append(created)
         root.append(expires)
-        return root
\ No newline at end of file
+        return root
diff --git a/suds/xsd/__init__.py b/suds/xsd/__init__.py
index 0917f3f..c5d8015 100644
--- a/suds/xsd/__init__.py
+++ b/suds/xsd/__init__.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -14,20 +14,10 @@
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
-"""
-The I{schema} module provides a intelligent representation of
-an XSD schema.  The I{raw} model is the XML tree and the I{model}
-is the denormalized, objectified and intelligent view of the schema.
-Most of the I{value-add} provided by the model is centered around
-tranparent referenced type resolution and targeted denormalization.
-"""
 
-from logging import getLogger
 from suds import *
 from suds.sax import Namespace, splitPrefix
 
-log = getLogger(__name__)
-
 
 def qualify(ref, resolvers, defns=Namespace.default):
     """
@@ -83,4 +73,3 @@ class Filter:
         else:
             result = ( x not in self.items )
         return result
-
diff --git a/suds/xsd/deplist.py b/suds/xsd/deplist.py
index 14ae19c..b813f80 100644
--- a/suds/xsd/deplist.py
+++ b/suds/xsd/deplist.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,18 +15,18 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{depsolve} module defines a class for performing dependancy solving.
+The I{depsolve} module defines a class for performing dependency solving.
 """
 
-from logging import getLogger
 from suds import *
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
 class DepList:
     """
-    Dependancy solving list.
+    Dependency solving list.
     Items are tuples: (object, (deps,))
     @ivar raw: The raw (unsorted) items.
     @type raw: list
@@ -48,7 +48,7 @@ class DepList:
         self.stack = []
         self.pushed = set()
         self.sorted = None
-        
+
     def add(self, *items):
         """
         Add items to be sorted.
@@ -62,10 +62,10 @@ class DepList:
             key = item[0]
             self.index[key] = item
         return self
-        
+
     def sort(self):
         """
-        Sort the list based on dependancies.
+        Sort the list based on dependencies.
         @return: The sorted items.
         @rtype: list
         """
@@ -73,7 +73,7 @@ class DepList:
         self.pushed = set()
         for item in self.unsorted:
             popped = []
-            self.push(item)            
+            self.push(item)
             while len(self.stack):
                 try:
                     top = self.top()
@@ -90,7 +90,7 @@ class DepList:
                 self.sorted.append(p)
         self.unsorted = self.sorted
         return self.sorted
-    
+
     def top(self):
         """
         Get the item at the top of the stack.
@@ -98,7 +98,7 @@ class DepList:
         @rtype: (item, iter)
         """
         return self.stack[-1]
-    
+
     def push(self, item):
         """
         Push and item onto the sorting stack.
@@ -112,7 +112,7 @@ class DepList:
         frame = (item, iter(item[1]))
         self.stack.append(frame)
         self.pushed.add(item)
-    
+
     def pop(self):
         """
         Pop the top item off the stack and append
@@ -137,4 +137,4 @@ if __name__ == '__main__':
     x = ('x', ())
     L = DepList()
     L.add(c, e, d, b, f, a, x)
-    print [x[0] for x in L.sort()]
\ No newline at end of file
+    print [x[0] for x in L.sort()]
diff --git a/suds/xsd/doctor.py b/suds/xsd/doctor.py
index d7bbc14..5a52e76 100644
--- a/suds/xsd/doctor.py
+++ b/suds/xsd/doctor.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -19,11 +19,11 @@ The I{doctor} module provides classes for fixing broken (sick)
 schema(s).
 """
 
-from logging import getLogger
-from suds.sax import splitPrefix, Namespace
+from suds.sax import Namespace
 from suds.sax.element import Element
 from suds.plugin import DocumentPlugin, DocumentContext
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -46,10 +46,10 @@ class Practice(Doctor):
     @ivar doctors: A list of doctors.
     @type doctors: list
     """
-    
+
     def __init__(self):
         self.doctors = []
-        
+
     def add(self, doctor):
         """
         Add a doctor to the practice
@@ -78,10 +78,10 @@ class TnsFilter:
         """
         self.tns = []
         self.add(*tns)
-        
+
     def add(self, *tns):
         """
-        Add I{targetNamesapces} to be added.
+        Add I{targetNamespaces} to be added.
         @param tns: A list of target namespaces.
         @type tns: [str,...]
         """
@@ -102,7 +102,7 @@ class TnsFilter:
             matched = 1
         itself = ( ns == tns )
         return ( matched and not itself )
-    
+
 
 class Import:
     """
@@ -119,7 +119,7 @@ class Import:
     """
 
     xsdns = Namespace.xsdns
-    
+
     def __init__(self, ns, location=None):
         """
         @param ns: An import namespace.
@@ -130,7 +130,7 @@ class Import:
         self.ns = ns
         self.location = location
         self.filter = TnsFilter()
-        
+
     def setfilter(self, filter):
         """
         Set the filter.
@@ -138,7 +138,7 @@ class Import:
         @type filter: L{TnsFilter}
         """
         self.filter = filter
-        
+
     def apply(self, root):
         """
         Apply the import (rule) to the specified schema.
@@ -157,7 +157,7 @@ class Import:
             node.set('schemaLocation', self.location)
         log.debug('inserting: %s', node)
         root.insert(node)
-        
+
     def add(self, root):
         """
         Add an <xs:import/> to the specified schema root.
@@ -169,12 +169,12 @@ class Import:
         if self.location is not None:
             node.set('schemaLocation', self.location)
         log.debug('%s inserted', node)
-        root.insert(node) 
-        
+        root.insert(node)
+
     def exists(self, root):
         """
         Check to see if the <xs:import/> already exists
-        in the specified schema root by matching I{namesapce}.
+        in the specified schema root by matching I{namespace}.
         @param root: A schema root.
         @type root: L{Element}
         """
@@ -185,7 +185,7 @@ class Import:
             if self.ns == ns:
                 return 1
         return 0
-    
+
 
 class ImportDoctor(Doctor, DocumentPlugin):
     """
@@ -195,19 +195,17 @@ class ImportDoctor(Doctor, DocumentPlugin):
     """
 
     def __init__(self, *imports):
-        """
-        """
         self.imports = []
         self.add(*imports)
-        
+
     def add(self, *imports):
         """
-        Add a namesapce to be checked.
+        Add a namespace to be checked.
         @param imports: A list of L{Import} objects.
         @type imports: [L{Import},..]
         """
         self.imports += imports
-        
+
     def examine(self, node):
         for imp in self.imports:
             imp.apply(node)
@@ -223,4 +221,3 @@ class ImportDoctor(Doctor, DocumentPlugin):
         for child in node:
             context.document = child
             self.parsed(context)
-        
\ No newline at end of file
diff --git a/suds/xsd/query.py b/suds/xsd/query.py
index c88b220..8f2266b 100644
--- a/suds/xsd/query.py
+++ b/suds/xsd/query.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -18,20 +18,20 @@
 The I{query} module defines a class for performing schema queries.
 """
 
-from logging import getLogger
 from suds import *
 from suds.sudsobject import *
 from suds.xsd import qualify, isqref
 from suds.xsd.sxbuiltin import Factory
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
 class Query(Object):
     """
     Schema query base class.
+
     """
-    
     def __init__(self, ref=None):
         """
         @param ref: The schema reference being queried.
@@ -44,18 +44,18 @@ class Query(Object):
         self.resolved = False
         if not isqref(self.ref):
             raise Exception('%s, must be qref' % tostr(self.ref))
-        
+
     def execute(self, schema):
         """
         Execute this query using the specified schema.
-        @param schema: The schema associated with the query.  The schema
-            is used by the query to search for items.
+        @param schema: The schema associated with the query. The schema is used
+            by the query to search for items.
         @type schema: L{schema.Schema}
         @return: The item matching the search criteria.
         @rtype: L{sxbase.SchemaObject}
         """
         raise Exception, 'not-implemented by subclass'
-        
+
     def filter(self, result):
         """
         Filter the specified result based on query criteria.
@@ -70,7 +70,7 @@ class Query(Object):
         if reject:
             log.debug('result %s, rejected by\n%s', Repr(result), self)
         return reject
-    
+
     def result(self, result):
         """
         Query result post processing.
@@ -89,11 +89,11 @@ class Query(Object):
 
 class BlindQuery(Query):
     """
-    Schema query class that I{blindly} searches for a reference in
-    the specified schema.  It may be used to find Elements and Types but
-    will match on an Element first.  This query will also find builtins.
+    Schema query class that I{blindly} searches for a reference in the
+    specified schema. It may be used to find Elements and Types but will match
+    on an Element first. This query will also find builtins.
+
     """
-        
     def execute(self, schema):
         if schema.builtin(self.ref):
             name = self.ref[0]
@@ -116,10 +116,10 @@ class BlindQuery(Query):
 
 class TypeQuery(Query):
     """
-    Schema query class that searches for Type references in
-    the specified schema.  Matches on root types only.
+    Schema query class that searches for Type references in the specified
+    schema. Matches on root types only.
+
     """
-        
     def execute(self, schema):
         if schema.builtin(self.ref):
             name = self.ref[0]
@@ -134,10 +134,10 @@ class TypeQuery(Query):
 
 class GroupQuery(Query):
     """
-    Schema query class that searches for Group references in
-    the specified schema.
+    Schema query class that searches for Group references in the specified
+    schema.
+
     """
-        
     def execute(self, schema):
         result = schema.groups.get(self.ref)
         if self.filter(result):
@@ -147,17 +147,17 @@ class GroupQuery(Query):
 
 class AttrQuery(Query):
     """
-    Schema query class that searches for Attribute references in
-    the specified schema.  Matches on root Attribute by qname first, then searches
-    deep into the document.
+    Schema query class that searches for Attribute references in the specified
+    schema. Matches on root Attribute by qname first, then searches deeper into
+    the document.
+
     """
-        
     def execute(self, schema):
         result = schema.attributes.get(self.ref)
         if self.filter(result):
             result = self.__deepsearch(schema)
         return self.result(result)
-    
+
     def __deepsearch(self, schema):
         from suds.xsd.sxbasic import Attribute
         result = None
@@ -172,10 +172,10 @@ class AttrQuery(Query):
 
 class AttrGroupQuery(Query):
     """
-    Schema query class that searches for attributeGroup references in
-    the specified schema.
+    Schema query class that searches for attributeGroup references in the
+    specified schema.
+
     """
-        
     def execute(self, schema):
         result = schema.agrps.get(self.ref)
         if self.filter(result):
@@ -185,17 +185,17 @@ class AttrGroupQuery(Query):
 
 class ElementQuery(Query):
     """
-    Schema query class that searches for Element references in
-    the specified schema.  Matches on root Elements by qname first, then searches
-    deep into the document.
+    Schema query class that searches for Element references in the specified
+    schema. Matches on root Elements by qname first, then searches deeper into
+    the document.
+
     """
-        
     def execute(self, schema):
         result = schema.elements.get(self.ref)
         if self.filter(result):
             result = self.__deepsearch(schema)
         return self.result(result)
-    
+
     def __deepsearch(self, schema):
         from suds.xsd.sxbasic import Element
         result = None
@@ -205,4 +205,4 @@ class ElementQuery(Query):
                 result = None
             else:
                 break
-        return result
\ No newline at end of file
+        return result
diff --git a/suds/xsd/schema.py b/suds/xsd/schema.py
index cb7d678..00e217f 100644
--- a/suds/xsd/schema.py
+++ b/suds/xsd/schema.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,15 +15,14 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{schema} module provides a intelligent representation of
+The I{schema} module provides an intelligent representation of
 an XSD schema.  The I{raw} model is the XML tree and the I{model}
 is the denormalized, objectified and intelligent view of the schema.
 Most of the I{value-add} provided by the model is centered around
-tranparent referenced type resolution and targeted denormalization.
+transparent referenced type resolution and targeted denormalization.
 """
 
 
-import suds.metrics
 from suds import *
 from suds.xsd import *
 from suds.xsd.sxbuiltin import *
@@ -33,14 +32,14 @@ from suds.xsd.sxbase import SchemaObject
 from suds.xsd.deplist import DepList
 from suds.sax.element import Element
 from suds.sax import splitPrefix, Namespace
-from logging import getLogger
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
-class SchemaCollection:
+class SchemaCollection(UnicodeMixin):
     """
-    A collection of schema objects.  This class is needed because WSDLs 
+    A collection of schema objects.  This class is needed because WSDLs
     may contain more then one <schema/> node.
     @ivar wsdl: A wsdl object.
     @type wsdl: L{suds.wsdl.Definitions}
@@ -49,7 +48,7 @@ class SchemaCollection:
     @ivar namespaces: A dictionary of contained schemas by namespace.
     @type namespaces: {str:L{Schema}}
     """
-    
+
     def __init__(self, wsdl):
         """
         @param wsdl: A wsdl object.
@@ -58,7 +57,7 @@ class SchemaCollection:
         self.wsdl = wsdl
         self.children = []
         self.namespaces = {}
-        
+
     def add(self, schema):
         """
         Add a schema node to the collection.  Schema(s) within the same target
@@ -74,7 +73,7 @@ class SchemaCollection:
         else:
             existing.root.children += schema.root.children
             existing.root.nsprefixes.update(schema.root.nsprefixes)
-        
+
     def load(self, options):
         """
         Load the schema objects for the root nodes.
@@ -97,7 +96,7 @@ class SchemaCollection:
         merged = self.merge()
         log.debug('MERGED:\n%s', merged)
         return merged
-        
+
     def autoblend(self):
         """
         Ensure that all schemas within the collection
@@ -109,7 +108,7 @@ class SchemaCollection:
         for s in self.children:
             for ns in namespaces:
                 tns = s.root.get('targetNamespace')
-                if  tns == ns:
+                if tns == ns:
                     continue
                 for imp in s.root.getChildren('import'):
                     if imp.get('namespace') == ns:
@@ -118,18 +117,18 @@ class SchemaCollection:
                 imp.set('namespace', ns)
                 s.root.append(imp)
         return self
-        
+
     def locate(self, ns):
         """
         Find a schema by namespace.  Only the URI portion of
         the namespace is compared to each schema's I{targetNamespace}
         @param ns: A namespace.
         @type ns: (prefix,URI)
-        @return: The schema matching the namesapce, else None.
+        @return: The schema matching the namespace, else None.
         @rtype: L{Schema}
         """
         return self.namespaces.get(ns[1])
-    
+
     def merge(self):
         """
         Merge the contained schemas into one.
@@ -143,13 +142,10 @@ class SchemaCollection:
             return schema
         else:
             return None
-    
+
     def __len__(self):
         return len(self.children)
-    
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-    
+
     def __unicode__(self):
         result = ['\nschema collection']
         for s in self.children:
@@ -157,7 +153,7 @@ class SchemaCollection:
         return '\n'.join(result)
 
 
-class Schema:
+class Schema(UnicodeMixin):
     """
     The schema is an objectification of a <schema/> (xsd) definition.
     It provides inspection, lookup and type resolution.
@@ -187,9 +183,9 @@ class Schema:
         (@elementFormDefault).
     @type form_qualified: bool
     """
-    
+
     Tag = 'schema'
-    
+
     def __init__(self, root, baseurl, options, container=None):
         """
         @param root: The xml root.
@@ -227,7 +223,7 @@ class Schema:
             log.debug('built:\n%s', self)
             self.dereference()
             log.debug('dereferenced:\n%s', self)
-                
+
     def mktns(self):
         """
         Make the schema's target namespace.
@@ -239,7 +235,7 @@ class Schema:
         if tns[1] is not None:
             tns[0] = self.root.findPrefix(tns[1])
         return tuple(tns)
-                
+
     def build(self):
         """
         Build the schema (object graph) using the root node
@@ -256,14 +252,14 @@ class Schema:
         self.types = collated[4]
         self.groups = collated[5]
         self.agrps = collated[6]
-        
+
     def merge(self, schema):
         """
         Merge the contents from the schema.  Only objects not already contained
         in this schema's collections are merged.  This is to provide for bidirectional
         import which produce cyclic includes.
         @returns: self
-        @rtype: L{Schema} 
+        @rtype: L{Schema}
         """
         for item in schema.attributes.items():
             if item[0] in self.attributes:
@@ -292,7 +288,7 @@ class Schema:
             self.agrps[item[0]] = item[1]
         schema.merged = True
         return self
-        
+
     def open_imports(self, options):
         """
         Instruct all contained L{sxbasic.Import} children to import
@@ -308,7 +304,7 @@ class Schema:
             imported.open_imports(options)
             log.debug('imported:\n%s', imported)
             self.merge(imported)
-            
+
     def dereference(self):
         """
         Instruct all children to perform dereferencing.
@@ -330,7 +326,7 @@ class Schema:
             d = deps[midx]
             log.debug('(%s) merging %s <== %s', self.tns[1], Repr(x), Repr(d))
             x.merge(d)
-        
+
     def locate(self, ns):
         """
         Find a schema by namespace.  Only the URI portion of
@@ -338,7 +334,7 @@ class Schema:
         The request is passed to the container.
         @param ns: A namespace.
         @type ns: (prefix,URI)
-        @return: The schema matching the namesapce, else None.
+        @return: The schema matching the namespace, else None.
         @rtype: L{Schema}
         """
         if self.container is not None:
@@ -352,20 +348,20 @@ class Schema:
         @param ref: A str or qref.
         @type ref: (str|qref)
         @return: True if B{not} a builtin, else False.
-        @rtype: bool 
+        @rtype: bool
         """
         if ref is None:
             return True
         else:
             return ( not self.builtin(ref, context) )
-    
+
     def builtin(self, ref, context=None):
         """
         Get whether the specified reference is an (xs) builtin.
         @param ref: A str or qref.
         @type ref: (str|qref)
         @return: True if builtin, else False.
-        @rtype: bool 
+        @rtype: bool
         """
         w3 = 'http://www.w3.org'
         try:
@@ -373,13 +369,13 @@ class Schema:
                 ns = ref[1]
                 return ( ref[0] in Factory.tags and ns.startswith(w3) )
             if context is None:
-                context = self.root    
+                context = self.root
             prefix = splitPrefix(ref)[0]
             prefixes = context.findPrefixes(w3, 'startswith')
             return ( prefix in prefixes and ref[0] in Factory.tags )
         except:
             return False
-        
+
     def instance(self, root, baseurl, options):
         """
         Create and return an new schema object using the
@@ -407,16 +403,9 @@ class Schema:
             result.append(c.str(indent+1))
         result.append('')
         return '\n'.join(result)
-        
+
     def __repr__(self):
-        myrep = '<%s tns="%s"/>' % (self.id, self.tns[1])
-        return myrep.encode('utf-8')
-    
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-    
+        return '<%s tns="%s"/>' % (self.id, self.tns[1])
+
     def __unicode__(self):
         return self.str()
-
-
-
diff --git a/suds/xsd/sxbase.py b/suds/xsd/sxbase.py
index 2577ffd..1ceb823 100644
--- a/suds/xsd/sxbase.py
+++ b/suds/xsd/sxbase.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -15,31 +15,29 @@
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 """
-The I{sxbase} module provides I{base} classes that represent
-schema objects.
+The I{sxbase} module provides I{base} classes representing schema objects.
 """
 
-from logging import getLogger
 from suds import *
 from suds.xsd import *
 from suds.sax.element import Element
 from suds.sax import Namespace
 
+from logging import getLogger
 log = getLogger(__name__)
 
 
-class SchemaObject(object):
+class SchemaObject(UnicodeMixin):
     """
-    A schema object is an extension to object object with
-    with schema awareness.
+    A schema object is an extension to object with schema awareness.
     @ivar root: The XML root element.
     @type root: L{Element}
     @ivar schema: The schema containing this object.
     @type schema: L{schema.Schema}
-    @ivar form_qualified: A flag that inidcates that @elementFormDefault
+    @ivar form_qualified: A flag that indicates that @elementFormDefault
         has a value of I{qualified}.
     @type form_qualified: boolean
-    @ivar nillable: A flag that inidcates that @nillable
+    @ivar nillable: A flag that indicates that @nillable
         has a value of I{true}.
     @type nillable: boolean
     @ivar default: The default value.
@@ -51,7 +49,7 @@ class SchemaObject(object):
     @classmethod
     def prepend(cls, d, s, filter=Filter()):
         """
-        Prepend schema object's from B{s}ource list to 
+        Prepend schema objects from B{s}ource list to
         the B{d}estination list while applying the filter.
         @param d: The destination list.
         @type d: list
@@ -65,11 +63,11 @@ class SchemaObject(object):
             if x in filter:
                 d.insert(i, x)
                 i += 1
-    
+
     @classmethod
     def append(cls, d, s, filter=Filter()):
         """
-        Append schema object's from B{s}ource list to 
+        Append schema objects from B{s}ource list to
         the B{d}estination list while applying the filter.
         @param d: The destination list.
         @type d: list
@@ -86,7 +84,7 @@ class SchemaObject(object):
         """
         @param schema: The containing schema.
         @type schema: L{schema.Schema}
-        @param root: The xml root node.
+        @param root: The XML root node.
         @type root: L{Element}
         """
         self.schema = schema
@@ -102,8 +100,7 @@ class SchemaObject(object):
         self.nillable = False
         self.default = root.get('default')
         self.rawchildren = []
-        self.cache = {}
-        
+
     def attributes(self, filter=Filter()):
         """
         Get only the attribute content.
@@ -117,7 +114,7 @@ class SchemaObject(object):
             if child.isattr() and child in filter:
                 result.append((child, ancestry))
         return result
-                
+
     def children(self, filter=Filter()):
         """
         Get only the I{direct} or non-attribute content.
@@ -131,10 +128,10 @@ class SchemaObject(object):
             if not child.isattr() and child in filter:
                 result.append((child, ancestry))
         return result
-                
+
     def get_attribute(self, name):
         """
-        Get (find) a I{non-attribute} attribute by name.
+        Get (find) an attribute by name.
         @param name: A attribute name.
         @type name: str
         @return: A tuple: the requested (attribute, ancestry).
@@ -142,9 +139,9 @@ class SchemaObject(object):
         """
         for child, ancestry in self.attributes():
             if child.name == name:
-                return (child, ancestry)
-        return (None, [])
-                
+                return child, ancestry
+        return None, []
+
     def get_child(self, name):
         """
         Get (find) a I{non-attribute} child by name.
@@ -155,12 +152,12 @@ class SchemaObject(object):
         """
         for child, ancestry in self.children():
             if child.any() or child.name == name:
-                return (child, ancestry)
-        return (None, [])
+                return child, ancestry
+        return None, []
 
     def namespace(self, prefix=None):
         """
-        Get this properties namespace
+        Get this property's namespace.
         @param prefix: The default prefix.
         @type prefix: str
         @return: The schema's target namespace
@@ -170,94 +167,92 @@ class SchemaObject(object):
         if ns[0] is None:
             ns = (prefix, ns[1])
         return ns
-    
+
     def default_namespace(self):
         return self.root.defaultNamespace()
-    
-    def unbounded(self):
+
+    def multi_occurrence(self):
         """
-        Get whether this node is unbounded I{(a collection)}
-        @return: True if unbounded, else False.
+        Get whether the node has multiple occurrences, i.e. is a I{collection}.
+        @return: True if it has, False if it has 1 occurrence at most.
         @rtype: boolean
         """
         max = self.max
         if max is None:
-            max = '1'
+            return False
         if max.isdigit():
-            return (int(max) > 1)
-        else:
-            return ( max == 'unbounded' )
-    
+            return int(max) > 1
+        return max == 'unbounded'
+
     def optional(self):
         """
         Get whether this type is optional.
         @return: True if optional, else False
         @rtype: boolean
         """
-        min = self.min
-        if min is None:
-            min = '1'
-        return ( min == '0' )
-    
+        return self.min == '0'
+
     def required(self):
         """
         Get whether this type is required.
         @return: True if required, else False
         @rtype: boolean
         """
-        return ( not self.optional() )
-        
-    
+        return not self.optional()
+
     def resolve(self, nobuiltin=False):
         """
-        Resolve and return the nodes true self.
-        @param nobuiltin: Flag indicates that resolution must
-            not continue to include xsd builtins.
+        Resolve the node's type reference and return the referenced type node.
+
+        Only schema objects that actually support 'having a type' custom
+        implement this interface while others simply resolve as themselves.
+        @param nobuiltin: Flag indicating whether resolving to an external XSD
+            builtin type should not be allowed.
         @return: The resolved (true) type.
         @rtype: L{SchemaObject}
         """
-        return self.cache.get(nobuiltin, self)
-    
+        return self
+
     def sequence(self):
         """
-        Get whether this is an <xs:sequence/>
+        Get whether this is a <xs:sequence/>.
         @return: True if <xs:sequence/>, else False
         @rtype: boolean
         """
         return False
-    
+
     def xslist(self):
         """
-        Get whether this is an <xs:list/>
+        Get whether this is a <xs:list/>.
         @return: True if any, else False
         @rtype: boolean
         """
         return False
-    
+
     def all(self):
         """
-        Get whether this is an <xs:all/>
+        Get whether this is an <xs:all/>.
         @return: True if any, else False
         @rtype: boolean
         """
         return False
-    
+
     def choice(self):
         """
-        Get whether this is n <xs:choice/>
+        Get whether this is a <xs:choice/>.
         @return: True if any, else False
         @rtype: boolean
         """
         return False
-        
+
     def any(self):
         """
-        Get whether this is an <xs:any/>
+        Get whether this is an <xs:any/>.
         @return: True if any, else False
         @rtype: boolean
         """
         return False
-    
+
     def builtin(self):
         """
         Get whether this is a schema-instance (xs) type.
@@ -265,7 +260,7 @@ class SchemaObject(object):
         @rtype: boolean
         """
         return False
-    
+
     def enum(self):
         """
         Get whether this is a simple-type containing an enumeration.
@@ -273,7 +268,7 @@ class SchemaObject(object):
         @rtype: boolean
         """
         return False
-    
+
     def isattr(self):
         """
         Get whether the object is a schema I{attribute} definition.
@@ -281,7 +276,7 @@ class SchemaObject(object):
         @rtype: boolean
         """
         return False
-    
+
     def extension(self):
         """
         Get whether the object is an extension of another type.
@@ -289,7 +284,7 @@ class SchemaObject(object):
         @rtype: boolean
         """
         return False
-    
+
     def restriction(self):
         """
         Get whether the object is an restriction of another type.
@@ -297,20 +292,20 @@ class SchemaObject(object):
         @rtype: boolean
         """
         return False
-    
+
     def mixed(self):
         """
         Get whether this I{mixed} content.
         """
         return False
-        
+
     def find(self, qref, classes=()):
         """
         Find a referenced type in self or children.
         @param qref: A qualified reference.
         @type qref: qref
         @param classes: A list of classes used to qualify the match.
-        @type classes: [I{class},...] 
+        @type classes: [I{class},...]
         @return: The referenced type.
         @rtype: L{SchemaObject}
         @see: L{qualify()}
@@ -327,12 +322,12 @@ class SchemaObject(object):
 
     def translate(self, value, topython=True):
         """
-        Translate a value (type) to/from a python type.
+        Translate a value (type) to/from a Python type.
         @param value: A value to translate.
         @return: The converted I{language} type.
         """
         return value
-    
+
     def childtags(self):
         """
         Get a list of valid child tag names.
@@ -340,15 +335,15 @@ class SchemaObject(object):
         @rtype: [str,...]
         """
         return ()
-    
+
     def dependencies(self):
         """
-        Get a list of dependancies for dereferencing.
-        @return: A merge dependancy index and a list of dependancies.
+        Get a list of dependencies for dereferencing.
+        @return: A merge dependency index and a list of dependencies.
         @rtype: (int, [L{SchemaObject},...])
         """
-        return (None, [])
-    
+        return None, []
+
     def autoqualified(self):
         """
         The list of I{auto} qualified attribute values.
@@ -357,13 +352,13 @@ class SchemaObject(object):
         @rtype: list
         """
         return ['type', 'ref']
-    
+
     def qualify(self):
         """
         Convert attribute values, that are references to other
-        objects, into I{qref}.  Qualfied using default document namespace.
-        Since many wsdls are written improperly: when the document does
-        not define a default namespace, the schema target namespace is used
+        objects, into I{qref}.  Qualified using the default document namespace.
+        Since many WSDLs are written improperly: when the document does
+        not define its default namespace, the schema target namespace is used
         to qualify references.
         """
         defns = self.root.defaultNamespace()
@@ -378,7 +373,7 @@ class SchemaObject(object):
             qref = qualify(ref, self.root, defns)
             log.debug('%s, convert %s="%s" to %s', self.id, a, ref, qref)
             setattr(self, a, qref)
-            
+
     def merge(self, other):
         """
         Merge another object as needed.
@@ -391,18 +386,17 @@ class SchemaObject(object):
                   'default',
                   'type',
                   'nillable',
-                  'form_qualified',):
+                  'form_qualified'):
             if getattr(self, n) is not None:
                 continue
             v = getattr(other, n)
             if v is None:
                 continue
             setattr(self, n, v)
-            
-            
+
     def content(self, collection=None, filter=Filter(), history=None):
         """
-        Get a I{flattened} list of this nodes contents.
+        Get a I{flattened} list of this node's contents.
         @param collection: A list to fill.
         @type collection: list
         @param filter: A filter used to constrain the result.
@@ -424,7 +418,7 @@ class SchemaObject(object):
         for c in self.rawchildren:
             c.content(collection, filter, history[:])
         return collection
-    
+
     def str(self, indent=0, history=None):
         """
         Get a string representation of this object.
@@ -433,14 +427,13 @@ class SchemaObject(object):
         @return: A string.
         @rtype: str
         """
-        if history is None: 
+        if history is None:
             history = []
         if self in history:
             return '%s ...' % Repr(self)
         history.append(self)
         tab = '%*s'%(indent*3, '')
-        result  = []
-        result.append('%s<%s' % (tab, self.id))
+        result = ['%s<%s' % (tab, self.id)]
         for n in self.description():
             if not hasattr(self, n):
                 continue
@@ -460,21 +453,18 @@ class SchemaObject(object):
         else:
             result.append(' />')
         return ''.join(result)
-    
+
     def description(self):
         """
         Get the names used for str() and repr() description.
-        @return:  A dictionary of relavent attributes.
+        @return:  A dictionary of relevant attributes.
         @rtype: [str,...]
         """
         return ()
-        
-    def __str__(self):
-        return unicode(self).encode('utf-8')
-            
+
     def __unicode__(self):
         return unicode(self.str())
-    
+
     def __repr__(self):
         s = []
         s.append('<%s' % self.id)
@@ -486,36 +476,41 @@ class SchemaObject(object):
                 continue
             s.append(' %s="%s"' % (n, v))
         s.append(' />')
-        myrep = ''.join(s)
-        return myrep.encode('utf-8')
-    
+        return ''.join(s)
+
     def __len__(self):
         n = 0
         for x in self: n += 1
         return n
-    
+
     def __iter__(self):
         return Iter(self)
-    
+
     def __getitem__(self, index):
+        """Returns a contained schema object referenced by its 0-based index.
+
+        Returns None if such an object does not exist.
+
+        """
         i = 0
         for c in self:
             if i == index:
                 return c
+            i += 1
 
 
 class Iter:
     """
-    The content iterator - used to iterate the L{Content} children.  The iterator
-    provides a I{view} of the children that is free of container elements
-    such as <sequence/> and <choice/>.
+    The content iterator - used to iterate the L{Content} children.  The
+    iterator provides a I{view} of the children that is free of container
+    elements such as <sequence/> and <choice/>.
     @ivar stack: A stack used to control nesting.
     @type stack: list
     """
-    
+
     class Frame:
         """ A content iterator frame. """
-        
+
         def __init__(self, sx):
             """
             @param sx: A schema object.
@@ -524,7 +519,7 @@ class Iter:
             self.sx = sx
             self.items = sx.rawchildren
             self.index = 0
-            
+
         def next(self):
             """
             Get the I{next} item in the frame's collection.
@@ -535,7 +530,7 @@ class Iter:
                 result = self.items[self.index]
                 self.index += 1
                 return result
-    
+
     def __init__(self, sx):
         """
         @param sx: A schema object.
@@ -543,7 +538,7 @@ class Iter:
         """
         self.stack = []
         self.push(sx)
-        
+
     def push(self, sx):
         """
         Create a frame and push the specified object.
@@ -551,7 +546,7 @@ class Iter:
         @type sx: L{SchemaObject}
         """
         self.stack.append(Iter.Frame(sx))
-        
+
     def pop(self):
         """
         Pop the I{top} frame.
@@ -563,7 +558,7 @@ class Iter:
             return self.stack.pop()
         else:
             raise StopIteration()
-        
+
     def top(self):
         """
         Get the I{top} frame.
@@ -575,7 +570,7 @@ class Iter:
             return self.stack[-1]
         else:
             raise StopIteration()
-    
+
     def next(self):
         """
         Get the next item.
@@ -591,19 +586,19 @@ class Iter:
                 return self.next()
             if isinstance(result, Content):
                 ancestry = [f.sx for f in self.stack]
-                return (result, ancestry)
+                return result, ancestry
             self.push(result)
             return self.next()
-    
+
     def __iter__(self):
         return self
 
 
 class XBuiltin(SchemaObject):
     """
-    Represents an (xsd) schema <xs:*/> node
+    Represents an (XSD) schema <xs:*/> node.
     """
-    
+
     def __init__(self, schema, name):
         """
         @param schema: The containing schema.
@@ -613,15 +608,12 @@ class XBuiltin(SchemaObject):
         SchemaObject.__init__(self, schema, root)
         self.name = name
         self.nillable = True
-            
+
     def namespace(self, prefix=None):
         return Namespace.xsdns
-    
+
     def builtin(self):
         return True
-    
-    def resolve(self, nobuiltin=False):
-        return self
 
 
 class Content(SchemaObject):
@@ -631,10 +623,10 @@ class Content(SchemaObject):
     """
     pass
 
-    
+
 class NodeFinder:
     """
-    Find nodes based on flexable criteria.  The I{matcher} is
+    Find nodes based on flexable criteria.  The I{matcher}
     may be any object that implements a match(n) method.
     @ivar matcher: An object used as criteria for match.
     @type matcher: I{any}.match(n)
@@ -650,7 +642,7 @@ class NodeFinder:
         """
         self.matcher = matcher
         self.limit = limit
-        
+
     def find(self, node, list):
         """
         Traverse the tree looking for matches.
@@ -666,4 +658,4 @@ class NodeFinder:
                 return
         for c in node.rawchildren:
             self.find(c, list)
-        return self
\ No newline at end of file
+        return self
diff --git a/suds/xsd/sxbasic.py b/suds/xsd/sxbasic.py
index 2506e04..f605088 100644
--- a/suds/xsd/sxbasic.py
+++ b/suds/xsd/sxbasic.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -19,17 +19,16 @@ The I{sxbasic} module provides classes that represent
 I{basic} schema objects.
 """
 
-from logging import getLogger
 from suds import *
 from suds.xsd import *
 from suds.xsd.sxbase import *
 from suds.xsd.query import *
-from suds.sax import splitPrefix, Namespace
+from suds.sax import Namespace
 from suds.transport import TransportError
 from suds.reader import DocumentReader
 from urlparse import urljoin
 
-
+from logging import getLogger
 log = getLogger(__name__)
 
 
@@ -39,21 +38,52 @@ class RestrictionMatcher:
     """
     def match(self, n):
         return isinstance(n, Restriction)
-    
+
 
 class TypedContent(Content):
     """
     Represents any I{typed} content.
     """
+
+    def __init__(self, *args, **kwargs):
+        Content.__init__(self, *args, **kwargs)
+        self.resolved_cache = {}
+
     def resolve(self, nobuiltin=False):
+        """
+        Resolve the node's type reference and return the referenced type node.
+
+        Returns self if the type is defined locally, e.g. as a <complexType>
+        subnode. Otherwise returns the referenced external node.
+        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+            types should not be allowed.
+        @return: The resolved (true) type.
+        @rtype: L{SchemaObject}
+        """
+        cached = self.resolved_cache.get(nobuiltin)
+        if cached is not None:
+            return cached
+        resolved = self.__resolve_type(nobuiltin)
+        self.resolved_cache[nobuiltin] = resolved
+        return resolved
+
+    def __resolve_type(self, nobuiltin=False):
+        """
+        Private resolve() worker without any result caching.
+        @param nobuiltin: Flag indicating whether resolving to XSD builtin
+            types should not be allowed.
+        @return: The resolved (true) type.
+        @rtype: L{SchemaObject}
+
+        Implementation note:
+          Note that there is no need for a recursive implementation here since
+        a node can reference an external type node but there is no way using
+        WSDL to then make that type node actually be a reference to a different
+        type node.
+        """
         qref = self.qref()
         if qref is None:
             return self
-        key = 'resolved:nb=%s' % nobuiltin
-        cached = self.cache.get(key)
-        if cached is not None:
-            return cached
-        result = self
         query = TypeQuery(qref)
         query.history = [self]
         log.debug('%s, resolving: %s\n using:%s', self.id, qref, query)
@@ -61,21 +91,15 @@ class TypedContent(Content):
         if resolved is None:
             log.debug(self.schema)
             raise TypeNotFound(qref)
-        self.cache[key] = resolved
-        if resolved.builtin():
-            if nobuiltin:
-                result = self
-            else:
-                result = resolved
-        else:
-            result = resolved.resolve(nobuiltin)
-        return result
-    
+        if resolved.builtin() and nobuiltin:
+            return self
+        return resolved
+
     def qref(self):
         """
-        Get the I{type} qualified reference to the referenced xsd type.
+        Get the I{type} qualified reference to the referenced XSD type.
         This method takes into account simple types defined through
-        restriction with are detected by determining that self is simple
+        restriction which are detected by determining that self is simple
         (len=0) and by finding a restriction child.
         @return: The I{type} qualified reference.
         @rtype: qref
@@ -93,32 +117,24 @@ class TypedContent(Content):
 
 class Complex(SchemaObject):
     """
-    Represents an (xsd) schema <xs:complexType/> node.
-    @cvar childtags: A list of valid child node names
+    Represents an (XSD) schema <xs:complexType/> node.
+    @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
     """
-        
+
     def childtags(self):
-        return (
-            'attribute', 
-            'attributeGroup', 
-            'sequence', 
-            'all', 
-            'choice', 
-            'complexContent',
-            'simpleContent', 
-            'any', 
-            'group')
+        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
+            'complexContent', 'simpleContent', 'any', 'group'
 
     def description(self):
         return ('name',)
-    
+
     def extension(self):
         for c in self.rawchildren:
             if c.extension():
                 return True
         return False
-    
+
     def mixed(self):
         for c in self.rawchildren:
             if isinstance(c, SimpleContent) and c.mixed():
@@ -128,18 +144,18 @@ class Complex(SchemaObject):
 
 class Group(SchemaObject):
     """
-    Represents an (xsd) schema <xs:group/> node.
-    @cvar childtags: A list of valid child node names
+    Represents an (XSD) schema <xs:group/> node.
+    @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
     """
-        
+
     def childtags(self):
-        return ('sequence', 'all', 'choice')
-        
+        return 'sequence', 'all', 'choice'
+
     def dependencies(self):
         deps = []
         midx = None
-        if self.ref is not None:     
+        if self.ref is not None:
             query = GroupQuery(self.ref)
             g = query.execute(self.schema)
             if g is None:
@@ -147,25 +163,25 @@ class Group(SchemaObject):
                 raise TypeNotFound(self.ref)
             deps.append(g)
             midx = 0
-        return (midx, deps)
-    
+        return midx, deps
+
     def merge(self, other):
         SchemaObject.merge(self, other)
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return ('name', 'ref',)
-    
+        return 'name', 'ref'
+
 
 class AttributeGroup(SchemaObject):
     """
-    Represents an (xsd) schema <xs:attributeGroup/> node.
-    @cvar childtags: A list of valid child node names
+    Represents an (XSD) schema <xs:attributeGroup/> node.
+    @cvar childtags: A list of valid child node names.
     @type childtags: (I{str},...)
     """
-        
+
     def childtags(self):
-        return ('attribute', 'attributeGroup')
+        return 'attribute', 'attributeGroup'
 
     def dependencies(self):
         deps = []
@@ -178,52 +194,52 @@ class AttributeGroup(SchemaObject):
                 raise TypeNotFound(self.ref)
             deps.append(ag)
             midx = 0
-        return (midx, deps)
-    
+        return midx, deps
+
     def merge(self, other):
         SchemaObject.merge(self, other)
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return ('name', 'ref',)
-    
+        return 'name', 'ref'
+
 
 class Simple(SchemaObject):
     """
-    Represents an (xsd) schema <xs:simpleType/> node
+    Represents an (XSD) schema <xs:simpleType/> node.
     """
 
     def childtags(self):
-        return ('restriction', 'any', 'list',)
-    
+        return 'restriction', 'any', 'list'
+
     def enum(self):
         for child, ancestry in self.children():
             if isinstance(child, Enumeration):
                 return True
         return False
-    
+
     def mixed(self):
         return len(self)
 
     def description(self):
         return ('name',)
-    
+
     def extension(self):
         for c in self.rawchildren:
             if c.extension():
                 return True
         return False
-    
+
     def restriction(self):
         for c in self.rawchildren:
             if c.restriction():
                 return True
         return False
-    
+
 
 class List(SchemaObject):
     """
-    Represents an (xsd) schema <xs:list/> node
+    Represents an (XSD) schema <xs:list/> node.
     """
 
     def childtags(self):
@@ -231,23 +247,23 @@ class List(SchemaObject):
 
     def description(self):
         return ('name',)
-    
+
     def xslist(self):
         return True
 
-   
+
 class Restriction(SchemaObject):
     """
-    Represents an (xsd) schema <xs:restriction/> node
+    Represents an (XSD) schema <xs:restriction/> node.
     """
-    
+
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
         self.ref = root.get('base')
 
     def childtags(self):
-        return ('enumeration', 'attribute', 'attributeGroup')
-    
+        return 'enumeration', 'attribute', 'attributeGroup'
+
     def dependencies(self):
         deps = []
         midx = None
@@ -260,8 +276,8 @@ class Restriction(SchemaObject):
             if not super.builtin():
                 deps.append(super)
                 midx = 0
-        return (midx, deps)
-    
+        return midx, deps
+
     def restriction(self):
         return True
 
@@ -269,26 +285,26 @@ class Restriction(SchemaObject):
         SchemaObject.merge(self, other)
         filter = Filter(False, self.rawchildren)
         self.prepend(self.rawchildren, other.rawchildren, filter)
-        
+
     def description(self):
         return ('ref',)
-    
-    
+
+
 class Collection(SchemaObject):
     """
-    Represents an (xsd) schema collection node:
+    Represents an (XSD) schema collection node:
         - sequence
         - choice
         - all
     """
 
     def childtags(self):
-        return ('element', 'sequence', 'all', 'choice', 'any', 'group')
+        return 'element', 'sequence', 'all', 'choice', 'any', 'group'
 
 
 class Sequence(Collection):
     """
-    Represents an (xsd) schema <xs:sequence/> node.
+    Represents an (XSD) schema <xs:sequence/> node.
     """
     def sequence(self):
         return True
@@ -296,14 +312,15 @@ class Sequence(Collection):
 
 class All(Collection):
     """
-    Represents an (xsd) schema <xs:all/> node.
+    Represents an (XSD) schema <xs:all/> node.
     """
     def all(self):
         return True
 
+
 class Choice(Collection):
     """
-    Represents an (xsd) schema <xs:choice/> node.
+    Represents an (XSD) schema <xs:choice/> node.
     """
     def choice(self):
         return True
@@ -311,18 +328,18 @@ class Choice(Collection):
 
 class ComplexContent(SchemaObject):
     """
-    Represents an (xsd) schema <xs:complexContent/> node.
+    Represents an (XSD) schema <xs:complexContent/> node.
     """
-        
+
     def childtags(self):
-        return ('attribute', 'attributeGroup', 'extension', 'restriction')
-    
+        return 'attribute', 'attributeGroup', 'extension', 'restriction'
+
     def extension(self):
         for c in self.rawchildren:
             if c.extension():
                 return True
         return False
-    
+
     def restriction(self):
         for c in self.rawchildren:
             if c.restriction():
@@ -332,46 +349,49 @@ class ComplexContent(SchemaObject):
 
 class SimpleContent(SchemaObject):
     """
-    Represents an (xsd) schema <xs:simpleContent/> node.
+    Represents an (XSD) schema <xs:simpleContent/> node.
     """
-        
+
     def childtags(self):
-        return ('extension', 'restriction')
-    
+        return 'extension', 'restriction'
+
     def extension(self):
         for c in self.rawchildren:
             if c.extension():
                 return True
         return False
-    
+
     def restriction(self):
         for c in self.rawchildren:
             if c.restriction():
                 return True
         return False
-    
+
     def mixed(self):
         return len(self)
 
 
 class Enumeration(Content):
     """
-    Represents an (xsd) schema <xs:enumeration/> node
+    Represents an (XSD) schema <xs:enumeration/> node.
     """
 
     def __init__(self, schema, root):
         Content.__init__(self, schema, root)
         self.name = root.get('value')
-        
+
+    def description(self):
+        return ('name',)
+
     def enum(self):
         return True
 
-    
+
 class Element(TypedContent):
     """
-    Represents an (xsd) schema <xs:element/> node.
+    Represents an (XSD) schema <xs:element/> node.
     """
-    
+
     def __init__(self, schema, root):
         TypedContent.__init__(self, schema, root)
         a = root.get('form')
@@ -381,7 +401,7 @@ class Element(TypedContent):
         if a is not None:
             self.nillable = ( a in ('1', 'true') )
         self.implany()
-            
+
     def implany(self):
         """
         Set the type as any when implicit.
@@ -395,69 +415,88 @@ class Element(TypedContent):
             self.root.isempty():
                 self.type = self.anytype()
         return self
-        
+
     def childtags(self):
-        return ('attribute', 'simpleType', 'complexType', 'any',)
-    
+        return 'attribute', 'simpleType', 'complexType', 'any'
+
     def extension(self):
         for c in self.rawchildren:
             if c.extension():
                 return True
         return False
-    
+
     def restriction(self):
         for c in self.rawchildren:
             if c.restriction():
                 return True
         return False
-    
+
     def dependencies(self):
         deps = []
         midx = None
-        if self.ref is not None:
-            query = ElementQuery(self.ref)
-            e = query.execute(self.schema)
-            if e is None:
-                log.debug(self.schema)
-                raise TypeNotFound(self.ref)
+        e = self.__deref()
+        if e is not None:
             deps.append(e)
             midx = 0
-        return (midx, deps)
-    
+        return midx, deps
+
     def merge(self, other):
         SchemaObject.merge(self, other)
         self.rawchildren = other.rawchildren
 
     def description(self):
-        return ('name', 'ref', 'type')
-        
+        return 'name', 'ref', 'type'
+
     def anytype(self):
         """ create an xsd:anyType reference """
-        p,u = Namespace.xsdns
+        p, u = Namespace.xsdns
         mp = self.root.findPrefix(u)
         if mp is None:
             mp = p
             self.root.addPrefix(p, u)
         return ':'.join((mp, 'anyType'))
 
+    def namespace(self, prefix=None):
+        """
+        Get this schema element's target namespace.
+
+        In case of reference elements, the target namespace is defined by the
+        referenced and not the referencing element node.
+
+        @param prefix: The default prefix.
+        @type prefix: str
+        @return: The schema element's target namespace
+        @rtype: (I{prefix},I{URI})
+        """
+        e = self.__deref()
+        if e is not None:
+            return e.namespace(prefix)
+        return super(Element, self).namespace()
+
+    def __deref(self):
+        if self.ref is None:
+            return
+        query = ElementQuery(self.ref)
+        e = query.execute(self.schema)
+        if e is None:
+            log.debug(self.schema)
+            raise TypeNotFound(self.ref)
+        return e
+
 
 class Extension(SchemaObject):
     """
-    Represents an (xsd) schema <xs:extension/> node.
+    Represents an (XSD) schema <xs:extension/> node.
     """
-    
+
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
         self.ref = root.get('base')
-        
+
     def childtags(self):
-        return ('attribute',
-                'attributeGroup', 
-                'sequence', 
-                'all', 
-                'choice', 
-                'group')
-        
+        return 'attribute', 'attributeGroup', 'sequence', 'all', 'choice',  \
+            'group'
+
     def dependencies(self):
         deps = []
         midx = None
@@ -470,23 +509,23 @@ class Extension(SchemaObject):
             if not super.builtin():
                 deps.append(super)
                 midx = 0
-        return (midx, deps)
+        return midx, deps
 
     def merge(self, other):
         SchemaObject.merge(self, other)
         filter = Filter(False, self.rawchildren)
         self.prepend(self.rawchildren, other.rawchildren, filter)
-        
+
     def extension(self):
-        return ( self.ref is not None )
+        return self.ref is not None
 
     def description(self):
         return ('ref',)
-    
+
 
 class Import(SchemaObject):
     """
-    Represents an (xsd) schema <xs:import/> node
+    Represents an (XSD) schema <xs:import/> node.
     @cvar locations: A dictionary of namespace locations.
     @type locations: dict
     @ivar ns: The imported namespace.
@@ -496,13 +535,13 @@ class Import(SchemaObject):
     @ivar opened: Opened and I{imported} flag.
     @type opened: boolean
     """
-    
+
     locations = {}
-    
+
     @classmethod
     def bind(cls, ns, location=None):
         """
-        Bind a namespace to a schema location (URI).  
+        Bind a namespace to a schema location (URI).
         This is used for imports that don't specify a schemaLocation.
         @param ns: A namespace-uri.
         @type ns: str
@@ -513,7 +552,7 @@ class Import(SchemaObject):
         if location is None:
             location = ns
         cls.locations[ns] = location
-    
+
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
         self.ns = (None, root.get('namespace'))
@@ -521,7 +560,7 @@ class Import(SchemaObject):
         if self.location is None:
             self.location = self.locations.get(self.ns[1])
         self.opened = False
-        
+
     def open(self, options):
         """
         Open and import the refrenced schema.
@@ -542,12 +581,10 @@ class Import(SchemaObject):
                 result = self.download(options)
         log.debug('imported:\n%s', result)
         return result
-    
+
     def locate(self):
         """ find the schema locally """
-        if self.ns[1] == self.schema.tns[1]:
-            return None
-        else:
+        if self.ns[1] != self.schema.tns[1]:
             return self.schema.locate(self.ns)
 
     def download(self, options):
@@ -565,29 +602,29 @@ class Import(SchemaObject):
             msg = 'imported schema (%s) at (%s), failed' % (self.ns[1], url)
             log.error('%s, %s', self.id, msg, exc_info=True)
             raise Exception(msg)
- 
+
     def description(self):
-        return ('ns', 'location')
-    
+        return 'ns', 'location'
+
 
 class Include(SchemaObject):
     """
-    Represents an (xsd) schema <xs:include/> node
+    Represents an (XSD) schema <xs:include/> node.
     @ivar location: The (optional) location.
     @type location: namespace-uri
     @ivar opened: Opened and I{imported} flag.
     @type opened: boolean
     """
-    
+
     locations = {}
-    
+
     def __init__(self, schema, root):
         SchemaObject.__init__(self, schema, root)
         self.location = root.get('schemaLocation')
         if self.location is None:
             self.location = self.locations.get(self.ns[1])
         self.opened = False
-        
+
     def open(self, options):
         """
         Open and include the refrenced schema.
@@ -620,7 +657,7 @@ class Include(SchemaObject):
             msg = 'include schema at (%s), failed' % url
             log.error('%s, %s', self.id, msg, exc_info=True)
             raise Exception(msg)
-        
+
     def __applytns(self, root):
         """ make sure included schema has same tns. """
         TNS = 'targetNamespace'
@@ -631,24 +668,24 @@ class Include(SchemaObject):
         else:
             if self.schema.tns[1] != tns:
                 raise Exception, '%s mismatch' % TNS
-                
- 
+
+
     def description(self):
-        return ('location')
+        return 'location'
+
 
-   
 class Attribute(TypedContent):
     """
-    Represents an (xsd) <attribute/> node
+    Represents an (XSD) <attribute/> node.
     """
 
     def __init__(self, schema, root):
         TypedContent.__init__(self, schema, root)
         self.use = root.get('use', default='')
-        
+
     def childtags(self):
         return ('restriction',)
-        
+
     def isattr(self):
         return True
 
@@ -659,9 +696,9 @@ class Attribute(TypedContent):
         @rtype: str
         """
         return self.root.get('default', default='')
-    
+
     def optional(self):
-        return ( self.use != 'required' )
+        return self.use != 'required'
 
     def dependencies(self):
         deps = []
@@ -674,46 +711,45 @@ class Attribute(TypedContent):
                 raise TypeNotFound(self.ref)
             deps.append(a)
             midx = 0
-        return (midx, deps)
-    
+        return midx, deps
+
     def description(self):
-        return ('name', 'ref', 'type')
+        return 'name', 'ref', 'type'
 
 
 class Any(Content):
     """
-    Represents an (xsd) <any/> node
+    Represents an (XSD) <any/> node.
     """
 
     def get_child(self, name):
         root = self.root.clone()
         root.set('note', 'synthesized (any) child')
         child = Any(self.schema, root)
-        return (child, [])
-    
+        return child, []
+
     def get_attribute(self, name):
         root = self.root.clone()
         root.set('note', 'synthesized (any) attribute')
         attribute = Any(self.schema, root)
-        return (attribute, [])
-    
+        return attribute, []
+
     def any(self):
         return True
-    
-    
+
+
 class Factory:
     """
     @cvar tags: A factory to create object objects based on tag.
     @type tags: {tag:fn,}
     """
 
-    tags =\
-    {
+    tags = {
         'import' : Import,
-        'include' : Include, 
+        'include' : Include,
         'complexType' : Complex,
         'group' : Group,
-        'attributeGroup' : AttributeGroup, 
+        'attributeGroup' : AttributeGroup,
         'simpleType' : Simple,
         'list' : List,
         'element' : Element,
@@ -728,18 +764,18 @@ class Factory:
         'extension' : Extension,
         'any' : Any,
     }
-    
+
     @classmethod
     def maptag(cls, tag, fn):
         """
         Map (override) tag => I{class} mapping.
-        @param tag: An xsd tag name.
+        @param tag: An XSD tag name.
         @type tag: str
         @param fn: A function or class.
         @type fn: fn|class.
         """
         cls.tags[tag] = fn
-    
+
     @classmethod
     def create(cls, root, schema):
         """
@@ -749,13 +785,11 @@ class Factory:
         @param schema: A schema object.
         @type schema: L{schema.Schema}
         @return: The created object.
-        @rtype: L{SchemaObject} 
+        @rtype: L{SchemaObject}
         """
         fn = cls.tags.get(root.name)
         if fn is not None:
             return fn(schema, root)
-        else:
-            return None
 
     @classmethod
     def build(cls, root, schema, filter=('*',)):
@@ -778,7 +812,7 @@ class Factory:
                 c = cls.build(node, schema, child.childtags())
                 child.rawchildren = c
         return children
-    
+
     @classmethod
     def collate(cls, children):
         imports = []
@@ -806,9 +840,7 @@ class Factory:
             types[c.qname] = c
         for i in imports:
             children.remove(i)
-        return (children, imports, attributes, elements, types, groups, agrps)
-
-    
+        return children, imports, attributes, elements, types, groups, agrps
 
 
 #######################################################
diff --git a/suds/xsd/sxbuiltin.py b/suds/xsd/sxbuiltin.py
index f8cf428..3d2c3dd 100644
--- a/suds/xsd/sxbuiltin.py
+++ b/suds/xsd/sxbuiltin.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -19,37 +19,34 @@ The I{sxbuiltin} module provides classes that represent
 XSD I{builtin} schema objects.
 """
 
-from logging import getLogger
 from suds import *
 from suds.xsd import *
 from suds.sax.date import *
 from suds.xsd.sxbase import XBuiltin
+
 import datetime as dt
 
 
-log = getLogger(__name__)
-    
-    
 class XString(XBuiltin):
     """
     Represents an (xsd) <xs:string/> node
     """
     pass
 
-  
+
 class XAny(XBuiltin):
     """
     Represents an (xsd) <any/> node
     """
-    
+
     def __init__(self, schema, name):
         XBuiltin.__init__(self, schema, name)
         self.nillable = False
-    
+
     def get_child(self, name):
         child = XAny(self.schema, name)
-        return (child, [])
-    
+        return child, []
+
     def any(self):
         return True
 
@@ -58,112 +55,99 @@ class XBoolean(XBuiltin):
     """
     Represents an (xsd) boolean builtin type.
     """
-    
-    translation = (
-        { '1':True,'true':True,'0':False,'false':False },
-        { True:'true',1:'true',False:'false',0:'false' },
-    )
-        
-    def translate(self, value, topython=True):
+
+    translation = ({'1':True, 'true':True, '0':False, 'false':False},
+        {True:'true', 1:'true', False:'false', 0:'false'})
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring):
                 return XBoolean.translation[0].get(value)
-            else:
-                return None
         else:
-            if isinstance(value, (bool,int)):
+            if isinstance(value, (bool, int)):
                 return XBoolean.translation[1].get(value)
-            else:
-                return value
+            return value
+
 
-   
 class XInteger(XBuiltin):
     """
     Represents an (xsd) xs:int builtin type.
     """
-        
-    def translate(self, value, topython=True):
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
                 return int(value)
-            else:
-                return None
         else:
             if isinstance(value, int):
                 return str(value)
-            else:
-                return value
-            
+            return value
+
+
 class XLong(XBuiltin):
     """
     Represents an (xsd) xs:long builtin type.
     """
-        
-    def translate(self, value, topython=True):
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
                 return long(value)
-            else:
-                return None
         else:
-            if isinstance(value, (int,long)):
+            if isinstance(value, (int, long)):
                 return str(value)
-            else:
-                return value
+            return value
+
 
-       
 class XFloat(XBuiltin):
     """
     Represents an (xsd) xs:float builtin type.
     """
-        
-    def translate(self, value, topython=True):
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
                 return float(value)
-            else:
-                return None
         else:
             if isinstance(value, float):
                 return str(value)
-            else:
-                return value
-            
+            return value
+
 
 class XDate(XBuiltin):
     """
     Represents an (xsd) xs:date builtin type.
     """
-        
-    def translate(self, value, topython=True):
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
-                return Date(value).date
-            else:
-                return None
+                return Date(value).value
         else:
             if isinstance(value, dt.date):
                 return str(Date(value))
-            else:
-                return value
+            return value
 
 
 class XTime(XBuiltin):
     """
     Represents an (xsd) xs:time builtin type.
     """
-        
-    def translate(self, value, topython=True):
+
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
-                return Time(value).time
-            else:
-                return None
+                return Time(value).value
         else:
-            if isinstance(value, dt.date):
+            if isinstance(value, dt.time):
                 return str(Time(value))
-            else:
-                return value
+            return value
 
 
 class XDateTime(XBuiltin):
@@ -171,19 +155,17 @@ class XDateTime(XBuiltin):
     Represents an (xsd) xs:datetime builtin type.
     """
 
-    def translate(self, value, topython=True):
+    @staticmethod
+    def translate(value, topython=True):
         if topython:
             if isinstance(value, basestring) and len(value):
-                return DateTime(value).datetime
-            else:
-                return None
+                return DateTime(value).value
         else:
-            if isinstance(value, dt.date):
+            if isinstance(value, dt.datetime):
                 return str(DateTime(value))
-            else:
-                return value
-            
-            
+            return value
+
+
 class Factory:
 
     tags =\
@@ -244,7 +226,7 @@ class Factory:
         # boolean
         'boolean' : XBoolean,
     }
-    
+
     @classmethod
     def maptag(cls, tag, fn):
         """
@@ -265,10 +247,9 @@ class Factory:
         @param name: The name.
         @type name: str
         @return: The created object.
-        @rtype: L{XBuiltin} 
+        @rtype: L{XBuiltin}
         """
         fn = cls.tags.get(name)
         if fn is not None:
             return fn(schema, name)
-        else:
-            return XBuiltin(schema, name)
+        return XBuiltin(schema, name)
diff --git a/suds_jurko.egg-info/PKG-INFO b/suds_jurko.egg-info/PKG-INFO
new file mode 100644
index 0000000..d4546ee
--- /dev/null
+++ b/suds_jurko.egg-info/PKG-INFO
@@ -0,0 +1,47 @@
+Metadata-Version: 1.1
+Name: suds-jurko
+Version: 0.6
+Summary: Lightweight SOAP client (Jurko's fork)
+Home-page: http://bitbucket.org/jurko/suds
+Author: Jurko Gospodnetic
+Author-email: jurko.gospodnetic at pke.hr
+License: (specified using classifiers)
+Download-URL: http://bitbucket.org/jurko/suds/downloads/suds-jurko-0.6.tar.bz2
+Description: 
+        ---------------------------------------
+        Lightweight SOAP client (Jurko's fork).
+        ---------------------------------------
+        
+          Based on the original 'suds' project by Jeff Ortel (jortel at redhat
+        dot com) hosted at 'http://fedorahosted.org/suds'.
+        
+          'Suds' is a lightweight SOAP-based web service client for Python
+        licensed under LGPL (see the LICENSE.txt file included in the
+        distribution).
+        
+          This is hopefully just a temporary fork of the original suds Python
+        library project created because the original project development seems
+        to have stalled. Should be reintegrated back into the original project
+        if it ever gets revived again.
+        
+        
+Keywords: SOAP,web,service,client
+Platform: (specified using classifiers)
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.4
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.0
+Classifier: Programming Language :: Python :: 3.1
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Topic :: Internet
+Obsoletes: suds
diff --git a/suds_jurko.egg-info/SOURCES.txt b/suds_jurko.egg-info/SOURCES.txt
new file mode 100644
index 0000000..1f259d3
--- /dev/null
+++ b/suds_jurko.egg-info/SOURCES.txt
@@ -0,0 +1,94 @@
+HACKING.rst
+LICENSE.txt
+MANIFEST.in
+README.rst
+TODO.txt
+ez_setup.py
+setup.cfg
+setup.py
+notes/argument_parsing.rst
+notes/readme.txt
+notes/traversing_client_data.rst
+suds/__init__.py
+suds/argparser.py
+suds/builder.py
+suds/cache.py
+suds/client.py
+suds/metrics.py
+suds/options.py
+suds/plugin.py
+suds/properties.py
+suds/reader.py
+suds/resolver.py
+suds/servicedefinition.py
+suds/serviceproxy.py
+suds/soaparray.py
+suds/store.py
+suds/sudsobject.py
+suds/version.py
+suds/wsdl.py
+suds/wsse.py
+suds/bindings/__init__.py
+suds/bindings/binding.py
+suds/bindings/document.py
+suds/bindings/multiref.py
+suds/bindings/rpc.py
+suds/mx/__init__.py
+suds/mx/appender.py
+suds/mx/basic.py
+suds/mx/core.py
+suds/mx/encoded.py
+suds/mx/literal.py
+suds/mx/typer.py
+suds/sax/__init__.py
+suds/sax/attribute.py
+suds/sax/date.py
+suds/sax/document.py
+suds/sax/element.py
+suds/sax/enc.py
+suds/sax/parser.py
+suds/sax/text.py
+suds/transport/__init__.py
+suds/transport/http.py
+suds/transport/https.py
+suds/transport/options.py
+suds/umx/__init__.py
+suds/umx/attrlist.py
+suds/umx/basic.py
+suds/umx/core.py
+suds/umx/encoded.py
+suds/umx/typed.py
+suds/xsd/__init__.py
+suds/xsd/deplist.py
+suds/xsd/doctor.py
+suds/xsd/query.py
+suds/xsd/schema.py
+suds/xsd/sxbase.py
+suds/xsd/sxbasic.py
+suds/xsd/sxbuiltin.py
+suds_jurko.egg-info/PKG-INFO
+suds_jurko.egg-info/SOURCES.txt
+suds_jurko.egg-info/dependency_links.txt
+suds_jurko.egg-info/top_level.txt
+tests/__init__.py
+tests/conftest.py
+tests/indirect_parametrize.py
+tests/test_argument_parser.py
+tests/test_cache.py
+tests/test_client_cache.py
+tests/test_date_time.py
+tests/test_document_store.py
+tests/test_input_parameters.py
+tests/test_reply_handling.py
+tests/test_request_construction.py
+tests/test_suds.py
+tests/test_timezone.py
+tests/test_transport.py
+tests/test_transport_http.py
+tests/external/__init__.py
+tests/external/axis1.py
+tests/external/axis2.py
+tests/external/jasper.py
+tests/external/public.py
+tests/external/rhq.py
+tests/external/saxenc.py
\ No newline at end of file
diff --git a/suds_jurko.egg-info/dependency_links.txt b/suds_jurko.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/suds_jurko.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/suds_jurko.egg-info/top_level.txt b/suds_jurko.egg-info/top_level.txt
new file mode 100644
index 0000000..ffb0ac3
--- /dev/null
+++ b/suds_jurko.egg-info/top_level.txt
@@ -0,0 +1,2 @@
+tests
+suds
diff --git a/tests/__init__.py b/tests/__init__.py
index 5aad187..8212858 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,25 +1,275 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-import sys
-import logging
-
-def setup_logging():
-    if sys.version_info < (2, 5):
-        fmt = '%(asctime)s [%(levelname)s] @%(filename)s:%(lineno)d\n%(message)s\n'
-    else:
-        fmt = '%(asctime)s [%(levelname)s] %(funcName)s() @%(filename)s:%(lineno)d\n%(message)s\n'
-    logging.basicConfig(level=logging.INFO, format=fmt)
\ No newline at end of file
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić( jurko.gospodnetic at pke.hr )
+
+import suds.client
+import suds.store
+
+
+def client_from_wsdl(wsdl_content, *args, **kwargs):
+    """
+    Constructs a non-caching suds Client based on the given WSDL content.
+
+      The wsdl_content is expected to be a raw byte string and not a unicode
+    string. This simple structure suits us fine here because XML content holds
+    its own embedded encoding identification ('utf-8' if not specified
+    explicitly).
+
+      Stores the content directly inside the suds library internal document
+    store under a hard-coded id to avoid having to load the data from a
+    temporary file.
+
+      Uses a locally created empty document store unless one is provided
+    externally using the 'documentStore' keyword argument.
+
+      Explicitly disables caching or otherwise, because we use the same
+    hardcoded id for our main WSDL document, suds would always reuse the first
+    such local document from its cache instead of fetching it from our document
+    store.
+
+    """
+    assert wsdl_content.__class__ is suds.byte_str_class
+    store = kwargs.get("documentStore")
+    if store is None:
+        store = suds.store.DocumentStore()
+        kwargs.update(documentStore=store)
+    testFileId = "whatchamacallit"
+    store.update({testFileId:wsdl_content})
+    kwargs.update(cache=None)
+    return suds.client.Client("suds://" + testFileId, *args, **kwargs)
+
+
+def compare_xml(lhs, rhs):
+    """
+    Compares two XML documents.
+
+    Not intended to be perfect, but only good enough comparison to be used
+    internally inside the project's test suite.
+
+    Does not compare namespace prefixes and considers them irrelevant. This is
+    because suds may generate different namespace prefixes for the same
+    underlying XML structure when used from different Python versions.
+
+    """
+    if lhs.__class__ is not suds.sax.document.Document:
+        return False
+    if rhs.__class__ is not suds.sax.document.Document:
+        return False
+    if len(lhs.getChildren()) != 1:
+        return False
+    if len(rhs.getChildren()) != 1:
+        return False
+    return compare_xml_element(lhs.getChildren()[0], rhs.getChildren()[0])
+
+
+def compare_xml_element(lhs, rhs):
+    """
+    Compares two XML elements.
+
+    Not intended to be perfect, but only good enough comparison to be used
+    internally inside the project's test suite.
+
+    Does not compare namespace prefixes and considers them irrelevant. This is
+    because suds may generate different namespace prefixes for the same
+    underlying XML structure when used from different Python versions.
+
+    Empty string & None XML element texts are considered the same to compensate
+    for different XML object tree construction methods representing 'no text'
+    elements differently, e.g. when constructed by the sax parser or when
+    constructed in code to represent a SOAP request.
+
+    """
+    if lhs.__class__ is not suds.sax.element.Element:
+        return False
+    if rhs.__class__ is not suds.sax.element.Element:
+        return False
+    if lhs.namespace()[1] != rhs.namespace()[1]:
+        return False
+    if lhs.name != rhs.name:
+        return False
+    lhs_text = lhs.text
+    rhs_text = rhs.text
+    if lhs_text == "":
+        lhs_text = None
+    if rhs_text == "":
+        rhs_text = None
+    if lhs_text != rhs_text:
+        return False
+    if len(lhs.getChildren()) != len(rhs.getChildren()):
+        return False
+    for l, r in zip(lhs.getChildren(), rhs.getChildren()):
+        if not compare_xml_element(l, r):
+            return False
+    return True
+
+
+def compare_xml_to_string(lhs, rhs):
+    """
+    Compares two XML documents, second one given as a string.
+
+    Not intended to be perfect, but only good enough comparison to be used
+    internally inside the project's test suite.
+
+    Does not compare namespace prefixes and considers them irrelevant. This is
+    because suds may generate different namespace prefixes for the same
+    underlying XML structure when used from different Python versions.
+
+    """
+    rhs_document = suds.sax.parser.Parser().parse(string=suds.byte_str(rhs))
+    return compare_xml(lhs, rhs_document)
+
+
+def runUsingPyTest(callerGlobals):
+    """Run the caller test script using the pytest testing framework."""
+    import sys
+    # Trick setuptools into not recognizing we are referencing __file__ here.
+    # If setuptools detects __file__ usage in a module, any package containing
+    # this module will be installed as an actual folder instead of a zipped
+    # archive. This __file__ usage is safe since it is used only when a script
+    # has been run directly, and that can not be done from a zipped package
+    # archive.
+    filename = callerGlobals.get("file".join(["__"] * 2))
+    if not filename:
+        sys.exit("Internal error: can not determine test script name.")
+    try:
+        import pytest
+    except ImportError:
+        filename = filename or "<unknown-script>"
+        sys.exit("'py.test' unit testing framework not available. Can not run "
+            "'%s' directly as a script." % (filename,))
+    exitCode = pytest.main(["--pyargs", filename] + sys.argv[1:])
+    sys.exit(exitCode)
+
+
+def wsdl_input(schema_content, *args, **kwargs):
+    """
+      Returns a WSDL schema used in different suds library tests, defining a
+    single operation taking an externally specified input structure and
+    returning no output.
+
+      The operation is named 'f' by default, but a custom name may be defined
+    for it by using a 'operation_name' keyword argument.
+
+      Initial input argument is the schema part of the WSDL, any remaining
+    positional arguments are interpreted as names of included top level input
+    parameter elements.
+
+    """
+    operation_name = kwargs.pop("operation_name", "f")
+    assert not kwargs
+
+    wsdl = ["""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+%s
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">""" % (schema_content,)]
+
+    assert len(args) >= 1
+    for arg in args:
+        wsdl.append("""\
+    <wsdl:part name="parameters" element="ns:%s" />""" % arg)
+
+    wsdl.append("""\
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="%(name)s">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="%(name)s">
+      <soap:operation soapAction="my-soap-action" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""" % {"name":operation_name})
+
+    return suds.byte_str("\n".join(wsdl))
+
+
+def wsdl_output(schema_content, *args):
+    """
+      Returns a WSDL schema used in different suds library tests, defining a
+    single operation named f, taking no input and returning an externally
+    specified output structure.
+
+      The first input parameter is the schema part of the WSDL, the rest of the
+    parameters identify top level output parameter elements.
+
+    """
+    wsdl = ["""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+%s
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fResponseMessage">""" % schema_content]
+
+    assert len(args) >= 1
+    for arg in args:
+        wsdl.append("""\
+    <wsdl:part name="parameters" element="ns:%s" />""" % arg)
+
+    wsdl.append("""\
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:output message="ns:fResponseMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""")
+
+    return suds.byte_str("\n".join(wsdl))
diff --git a/tests/builtin.py b/tests/builtin.py
deleted file mode 100644
index ca6c2e6..0000000
--- a/tests/builtin.py
+++ /dev/null
@@ -1,549 +0,0 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-import sys
-sys.path.append('../')
-import unittest
-from suds.sax.date import Timezone as Tz
-from suds.xsd.sxbuiltin import *
-from unittest import TestCase
-from tests import *
-
-setup_logging()
-
-
-class Date(XDate):
-    def __init__(self):
-        pass
-class Time(XTime):
-    def __init__(self):
-        pass
-class DateTime(XDateTime):
-    def __init__(self):
-        pass
-    
-class DateTest(TestCase):
-    
-    def testSimple(self):
-        ref = dt.date(1941, 12, 7)
-        s = '%.4d-%.2d-%.2d' % (ref.year, ref.month, ref.day)
-        xdate = Date()
-        d = xdate.translate(s)
-        self.assertEqual(d, ref)
-        
-    def testNegativeTimezone(self):
-        self.equalsTimezone(-6)
-        
-    def testPositiveTimezone(self):
-        self.equalsTimezone(6)
-        
-    def testUtcTimezone(self):
-        Timezone.LOCAL = 0
-        ref = dt.date(1941, 12, 7)
-        s = '%.4d-%.2d-%.2dZ' % (ref.year, ref.month, ref.day)
-        xdate = Date()
-        d = xdate.translate(s)
-        self.assertEqual(d, ref)
-        
-    def equalsTimezone(self, tz):
-        Timezone.LOCAL = tz
-        ref = dt.date(1941, 12, 7)
-        s = '%.4d-%.2d-%.2d%+.2d:00' % (ref.year, ref.month, ref.day, tz)
-        xdate = Date()
-        d = xdate.translate(s)
-        self.assertEqual(d, ref)
-
-
-  
-class TimeTest(TestCase):
-
-    def testSimple(self):
-        ref = dt.time(10, 30, 22)
-        s = '%.2d:%.2d:%.2d' % (ref.hour, ref.minute, ref.second)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testSimpleWithShortMicrosecond(self):
-        ref = dt.time(10, 30, 22, 34)
-        s = '%.2d:%.2d:%.2d.%4.d' % (ref.hour, ref.minute, ref.second, ref.microsecond)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testSimpleWithMicrosecond(self):
-        ref = dt.time(10, 30, 22, 999999)
-        s = '%.2d:%.2d:%.2d.%4.d' % (ref.hour, ref.minute, ref.second, ref.microsecond)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testSimpleWithLongMicrosecond(self):
-        ref = dt.time(10, 30, 22, 999999)
-        s = '%.2d:%.2d:%.2d.%4.d' % (ref.hour, ref.minute, ref.second, int('999999999'))
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testPositiveTimezone(self):
-        self.equalsTimezone(6)
-        
-    def testNegativeTimezone(self):
-        self.equalsTimezone(-6)
-        
-    def testUtcTimezone(self):
-        Timezone.LOCAL = 0
-        ref = dt.time(10, 30, 22)
-        s = '%.2d:%.2d:%.2dZ' % (ref.hour, ref.minute, ref.second)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def equalsTimezone(self, tz):
-        Timezone.LOCAL = tz
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, tz)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testConvertNegativeToGreaterNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, -5)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour-1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToLesserNegative(self):
-        Timezone.LOCAL = -5
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, -6)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour+1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToGreaterPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, 2)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour+1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToLesserPositive(self):
-        Timezone.LOCAL = 2
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, 3)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour-1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, 3)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour-9, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, -6)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour+9, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToUtc(self):
-        Timezone.LOCAL = 0
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, -6)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour+6, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToUtc(self):
-        Timezone.LOCAL = 0
-        ref = dt.time(10, 30, 22)
-        s = self.strTime(ref.hour, ref.minute, ref.second, 3)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour-3, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertUtcToPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.time(10, 30, 22)
-        s = '%.2d:%.2d:%.2dZ' % (ref.hour, ref.minute, ref.second)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour+3, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertUtcToNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.time(10, 30, 22)
-        s = '%.2d:%.2d:%.2dZ' % (ref.hour, ref.minute, ref.second)
-        xtime = Time()
-        t = xtime.translate(s)
-        self.assertEqual(ref.hour-6, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def strTime(self, h, m, s, offset):
-        return '%.2d:%.2d:%.2d%+.2d:00' % (h, m, s, offset)
-
-
-class DateTimeTest(TestCase):
-
-    def testSimple(self):
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2d' \
-            % (ref.year,
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testOverflow(self):
-        ref = dt.datetime(1, 1, 1, 0, 0, 0)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ' \
-            % (ref.year,
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testSimpleWithMicrosecond(self):
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22, 454)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2d.%.4d' \
-            % (ref.year, 
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second, 
-               ref.microsecond)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testPositiveTimezone(self):
-        self.equalsTimezone(6)
-        
-    def testNegativeTimezone(self):
-        self.equalsTimezone(-6)
-        
-    def testUtcTimezone(self):
-        Timezone.LOCAL = 0
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2d' \
-            % (ref.year,
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(t, ref)
-        
-    def equalsTimezone(self, tz):
-        Timezone.LOCAL = tz
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                tz)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(t, ref)
-        
-    def testConvertNegativeToGreaterNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -5)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour-1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToLesserNegative(self):
-        Timezone.LOCAL = -5
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -6)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour+1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToGreaterPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                2)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour+1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToLesserPositive(self):
-        Timezone.LOCAL = 2
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                3)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour-1, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                3)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour-9, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -6)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour+9, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToUtc(self):
-        Timezone.LOCAL = 0
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -6)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour+6, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertPositiveToUtc(self):
-        Timezone.LOCAL = 0
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                3)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour-3, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertUtcToPositive(self):
-        Timezone.LOCAL = 3
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ' \
-            % (ref.year,
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour+3, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertUtcToNegative(self):
-        Timezone.LOCAL = -6
-        ref = dt.datetime(1941, 12, 7, 10, 30, 22)
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ' \
-            % (ref.year,
-               ref.month, 
-               ref.day, 
-               ref.hour, 
-               ref.minute, 
-               ref.second)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(ref.day, t.day)
-        self.assertEqual(ref.hour-6, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToGreaterNegativeAndPreviousDay(self):
-        Timezone.LOCAL = -6
-        ref = dt.datetime(1941, 12, 7, 0, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -5)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(6, t.day)
-        self.assertEqual(23, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def testConvertNegativeToLesserNegativeAndNextDay(self):
-        Timezone.LOCAL = -5
-        ref = dt.datetime(1941, 12, 7, 23, 30, 22)
-        s = self.strDateTime(
-                ref.year,
-                ref.month,
-                ref.day,
-                ref.hour,
-                ref.minute,
-                ref.second,
-                -6)
-        xdt = DateTime()
-        t = xdt.translate(s)
-        self.assertEqual(ref.year, t.year)
-        self.assertEqual(ref.month, t.month)
-        self.assertEqual(8, t.day)
-        self.assertEqual(0, t.hour)
-        self.assertEqual(ref.minute, t.minute)
-        self.assertEqual(ref.second, t.second)
-        
-    def strDateTime(self, Y, M, D, h, m, s, offset):
-        s = '%.4d-%.2d-%.2dT%.2d:%.2d:%.2d%+.2d:00' \
-            % (Y, M, D, h, m, s, offset)
-        return s
-
-        
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..c706614
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+pytest configuration file for the suds test suite.
+
+"""
+
+# Make pytest load custom plugins expected to be loaded in our test suite.
+#TODO: pytest (tested up to version 2.5.1) will not display our plugin marker
+# information in its --markers list if called from a folder other than the one
+# containing the tests folder or if the tests folder is not on the current
+# Python path, e.g. if using pytest in the Python 3 implementation 'build'
+# folder constructed by 'setup.py build' using 'py.test build --markers'. The
+# plugin will still get loaded correctly when actually running the tests. This
+# has already been reported as a pytest issue.
+pytest_plugins = "tests.indirect_parametrize"
diff --git a/suds/bindings/__init__.py b/tests/external/__init__.py
similarity index 85%
copy from suds/bindings/__init__.py
copy to tests/external/__init__.py
index 5471eba..bacf155 100644
--- a/suds/bindings/__init__.py
+++ b/tests/external/__init__.py
@@ -1,6 +1,8 @@
+# -*- coding: utf-8 -*-
+
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
@@ -12,9 +14,4 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# written by: Jeff Ortel ( jortel at redhat.com )
-
-"""
-Provides modules containing classes to support Web Services (SOAP)
-bindings.
-"""
\ No newline at end of file
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
diff --git a/tests/axis1.py b/tests/external/axis1.py
similarity index 90%
rename from tests/axis1.py
rename to tests/external/axis1.py
index 5c72867..4c74c30 100644
--- a/tests/axis1.py
+++ b/tests/external/axis1.py
@@ -1,29 +1,26 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 #
-# This test requires installation or visability to my local axis(1) server.
+# This test requires installation or visibility to my local axis(1) server.
 #
 
 import sys
-sys.path.append('../')
+sys.path.append('../../')
 
-import logging
 import traceback as tb
-import suds.metrics as metrics
 from tests import *
 from suds import WebFault
 from suds.client import Client
@@ -31,57 +28,54 @@ from suds.sudsobject import Object
 from suds.transport.https import HttpAuthenticated
 from suds.plugin import *
 
-errors = 0
 
+errors = 0
 credentials = dict(username='jortel', password='abc123')
 
-setup_logging()
-
 
 class MyInitPlugin(InitPlugin):
 
     def initialized(self, context):
         print 'PLUGIN (init): initialized: ctx=%s' % context.__dict__
 
-    
+
 class MyDocumentPlugin(DocumentPlugin):
-    
+
     def loaded(self, context):
         print 'PLUGIN (document): loaded: ctx=%s' % context.__dict__
 
     def parsed(self, context):
         print 'PLUGIN (document): parsed: ctx=%s' % context.__dict__
 
-        
+
 class MyMessagePlugin(MessagePlugin):
-        
+
     def marshalled(self, context):
         print 'PLUGIN (message): marshalled: ctx=%s' % context.__dict__
-    
+
     def sending(self, context):
         print 'PLUGIN (message): sending: ctx=%s' % context.__dict__
 
     def received(self, context):
         print 'PLUGIN (message): received: ctx=%s' % context.__dict__
-        
+
     def parsed(self, context):
         print 'PLUGIN (message): parsed: ctx=%s' % context.__dict__
-        
+
     def unmarshalled(self, context):
         print 'PLUGIN: (massage): unmarshalled: ctx=%s' % context.__dict__
-        
-        
+
+
 myplugins = (
     MyInitPlugin(),
     MyDocumentPlugin(),
-    MyMessagePlugin(),)
-
+    MyMessagePlugin(),
+)
 
-#logging.getLogger('suds.client').setLevel(logging.DEBUG)
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n' 
+    print '\n________________________________________________________________\n'
     print 'Test @ ( %s )\nerrors = %d\n' % (url, errors)
 
 try:
@@ -137,7 +131,7 @@ try:
     print 'addPersion()'
     result = client.service.addPerson(person)
     print '\nreply(\n%s\n)\n' % str(result)
-    
+
     #
     # Async
     #
@@ -149,7 +143,7 @@ try:
     error.httpcode = '500'
     client.options.nosend=False
 #    request.failed(error)
-    
+
     #
     #
     # create a new name object used to update the person
@@ -182,7 +176,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = 'http://localhost:8081/axis/services/basic-rpc-encoded?wsdl'
     start(url)
@@ -245,7 +239,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     print "echo(' this is cool ')"
     result = client.service.echo('this is cool')
@@ -261,7 +255,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     print 'hello()'
     result = client.service.hello()
@@ -326,7 +320,7 @@ except Exception, e:
     tb.print_exc()
 
 try:
-    print 'testExceptions()' 
+    print 'testExceptions()'
     result = client.service.throwException()
     print '\nreply( %s )\n' % tostr(result)
     raise Exception('Fault expected and not raised')
@@ -353,5 +347,5 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 print '\nFinished: errors=%d' % errors
diff --git a/tests/axis2.py b/tests/external/axis2.py
similarity index 83%
rename from tests/axis2.py
rename to tests/external/axis2.py
index 8ed0bdd..0a64a8f 100644
--- a/tests/axis2.py
+++ b/tests/external/axis2.py
@@ -1,38 +1,29 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 import sys
-sys.path.append('../')
+sys.path.append('../../')
 
-import logging
-import traceback as tb
-import suds.metrics as metrics
 from tests import *
 from suds import *
 from suds.client import Client
 from datetime import datetime
 
-errors = 0
-
-setup_logging()
-
-#logging.getLogger('suds.client').setLevel(logging.DEBUG)
 
+errors = 0
 url = 'http://localhost:8080/axis2/services/BasicService?wsdl'
-    
 print 'url=%s' % url
 
 #
@@ -200,7 +191,7 @@ try:
     print '\nreply( %s )\n' % tostr(result)
 except Exception, e:
     print e
-    
+
 #
 # test faults
 #
@@ -212,4 +203,4 @@ try:
 except Exception, e:
     print e
 
-print '\nfinished: errors=%d' % errors
\ No newline at end of file
+print '\nfinished: errors=%d' % errors
diff --git a/tests/jasper.py b/tests/external/jasper.py
similarity index 52%
rename from tests/jasper.py
rename to tests/external/jasper.py
index 79389a1..fde34d5 100644
--- a/tests/jasper.py
+++ b/tests/external/jasper.py
@@ -1,41 +1,34 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 import sys
-sys.path.append('../')
+sys.path.append('../../')
 
-import logging
-import traceback as tb
-import urllib2
-import suds.metrics as metrics
 import traceback as tb
 from tests import *
 from suds import WebFault
 from suds.client import Client
 
-errors = 0
 
-setup_logging()
+errors = 0
 
-#logging.getLogger('suds.client').setLevel(logging.DEBUG)
 
 def start(url):
-    print '\n________________________________________________________________\n' 
+    print '\n________________________________________________________________\n'
     print 'Test @ ( %s )' % url
-    
+
 try:
     url = 'http://localhost:9090/jasperserver-pro/services/repository?wsdl'
     start(url)
diff --git a/tests/public.py b/tests/external/public.py
similarity index 86%
rename from tests/public.py
rename to tests/external/public.py
index 5132c7f..c1b9666 100644
--- a/tests/public.py
+++ b/tests/external/public.py
@@ -1,41 +1,34 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 import sys
-sys.path.append('../')
+sys.path.append('../../')
 
-import logging
 import traceback as tb
 import suds.metrics as metrics
 from tests import *
 from suds import WebFault
 from suds.client import Client
 
-errors = 0
-
-setup_logging()
 
-#logging.getLogger('suds.client').setLevel(logging.DEBUG)
-#logging.getLogger('suds.metrics').setLevel(logging.DEBUG)
-#logging.getLogger('suds').setLevel(logging.DEBUG)
+errors = 0
 
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n' 
+    print '\n________________________________________________________________\n'
     print 'Test @ ( %s ) %d' % (url, errors)
 
 try:
@@ -47,7 +40,7 @@ try:
     input = "42"
     d = dict(inputString=input)
     result = client.service.echoString(**d)
-    print 'echoString() =  %s' % result
+    print 'echoString() = %s' % result
     assert result == input
     # int
     input = 42
@@ -74,11 +67,11 @@ except WebFault, f:
     errors += 1
     print f
     print f.fault
-except Exception, e: 
+except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = 'http://jira.atlassian.com/rpc/soap/jirasoapservice-v2?wsdl'
     start(url)
@@ -92,11 +85,11 @@ except WebFault, f:
     errors += 1
     print f
     print f.fault
-except Exception, e: 
+except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = 'http://jira.atlassian.com/rpc/soap/jirasoapservice-v2?wsdl'
     start(url+'  ** cloned **')
@@ -110,11 +103,11 @@ except WebFault, f:
     errors += 1
     print f
     print f.fault
-except Exception, e: 
+except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = ' http://www.boyzoid.com/comp/randomQuote.cfc?wsdl '
     start(url)
@@ -129,7 +122,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = 'http://www.zenfolio.com/zf/api/zfapi.asmx?wsdl'
     start(url)
@@ -149,7 +142,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = 'http://cert.synxis.com/interface/ChannelConnect.asmx?WSDL'
     start(url)
@@ -247,7 +240,7 @@ except Exception, e:
     errors += 1
     print e
     tb.print_exc()
-    
+
 try:
     url = "http://arcweb.esri.com/services/v2/RouteFinder.wsdl"
     start(url)
diff --git a/tests/rhq.py b/tests/external/rhq.py
similarity index 83%
rename from tests/rhq.py
rename to tests/external/rhq.py
index 2b755d1..c17d578 100644
--- a/tests/rhq.py
+++ b/tests/external/rhq.py
@@ -1,17 +1,16 @@
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
-# License, or (at your option) any later version.
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Library Lesser General Public License for more details at
-# ( http://www.gnu.org/licenses/lgpl.html ).
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
 #
 # You should have received a copy of the GNU Lesser General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 # written by: Jeff Ortel ( jortel at redhat.com )
 
 #
@@ -20,35 +19,27 @@
 #
 
 import sys
-sys.path.append('../')
+sys.path.append('../../')
 
-import logging
 import traceback as tb
-import suds.metrics as metrics
 from tests import *
-from suds import null, WebFault
+from suds import WebFault
 from suds.client import Client
 
 
 errors = 0
 
-setup_logging()
-
-logging.getLogger('suds.client').setLevel(logging.DEBUG)
-#logging.getLogger('suds.metrics').setLevel(logging.DEBUG)
-#logging.getLogger('suds').setLevel(logging.DEBUG)
-
 
 def start(url):
     global errors
-    print '\n________________________________________________________________\n' 
+    print '\n________________________________________________________________\n'
     print 'Test @ ( %s ) %d' % (url, errors)
 
 
 def rhqTest():
-    
+
     global errors
-    
+
     url = 'http://localhost.localdomain:7080/rhq-rhq-enterprise-server-ejb3/WebservicesManagerBean?wsdl'
     start(url)
     client = Client(url)
@@ -93,7 +84,7 @@ def rhqTest():
         person.phone.append(phoneB)
         person.pet.append(dog)
         person.pet.append(cat)
-        print person       
+        print person
         #
         # addPerson()
         #
@@ -124,7 +115,7 @@ def rhqTest():
         errors += 1
         print e
         tb.print_exc()
-  
+
     try:
         print "echo('this is cool')"
         result = client.service.echo('this is cool')
@@ -140,7 +131,7 @@ def rhqTest():
         errors += 1
         print e
         tb.print_exc()
-        
+
     try:
         print 'hello()'
         result = client.service.hello()
@@ -180,7 +171,7 @@ def rhqTest():
         errors += 1
         print e
         tb.print_exc()
-    
+
     try:
         s = 'hello'
         for n in range(0, 3):
@@ -198,9 +189,9 @@ def rhqTest():
         errors += 1
         print e
         tb.print_exc()
-    
+
     try:
-        print 'testExceptions()' 
+        print 'testExceptions()'
         result = client.service.testExceptions()
         print '\nreply( %s )\n' % tostr(result)
         raise Exception('Fault expected and not raised')
@@ -213,7 +204,7 @@ def rhqTest():
         print e
         tb.print_exc()
 
-        
+
 if __name__ == '__main__':
     errors = 0
     rhqTest()
diff --git a/tests/saxenc.py b/tests/external/saxenc.py
similarity index 99%
rename from tests/saxenc.py
rename to tests/external/saxenc.py
index e88e335..1bb351c 100644
--- a/tests/saxenc.py
+++ b/tests/external/saxenc.py
@@ -1,6 +1,6 @@
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the (LGPL) GNU Lesser General Public License as
-# published by the Free Software Foundation; either version 3 of the 
+# published by the Free Software Foundation; either version 3 of the
 # License, or (at your option) any later version.
 #
 # This program is distributed in the hope that it will be useful,
diff --git a/tests/indirect_parametrize.py b/tests/indirect_parametrize.py
new file mode 100644
index 0000000..4c909c0
--- /dev/null
+++ b/tests/indirect_parametrize.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Indirect parametrization pytest plugin.
+
+Allows tests parametrization data to be generated using a function instead of
+having to be explicitly specified as is the case when using the builtin
+pytest.mark.parametrize functionality.
+
+Interface is similar to the builtin pytest.mark.parametrize implementation,
+except that it takes an indirect parametrization function as an additional
+initial positional argument. All the other arguments parameters are forwarded
+onto this indirect parametrization function which then calculates and returns
+the actual parametrization data. The return values are a standard Python
+positional argument list and keyword argument dictionary to be used for the
+underlying metafunc.parametrize() call.
+
+May be used as either:
+  1. pytest.indirect_parametrizer() or
+  2. pytest.mark.indirect_parametrizer().
+
+The two usages are equivalent except that the first one may be used with any
+indirect parametrization function while the second one can not be used with
+indirect parametrization functions taking no input parameters (with pytest
+versions prior to 2.5.2 it also can not be used with functions taking only
+keyword arguments). This is a technical restriction based on an underlying
+pytest.mark implementation detail. See the indirect_parametrize() function
+doc-string for more detailed information.
+
+Usage example making the following test_example() test function calls after
+calculating them from a much shorter definition:
+ * test_example("Fritula", 1)
+ * test_example("Fritula", 2)
+ * test_example("Fritula", 3)
+ * test_example("Fritula", 4)
+ * test_example("Fritula", 5)
+ * test_example("Fritula", 6)
+ * test_example("Fritula", 7)
+ * test_example("Fritula", 8)
+ * test_example("Fritula", 9)
+ * test_example("Madagascar", 10)
+ * test_example("Madagascar", 20)
+ * test_example("Madagascar", 30)
+ * test_example("Rumpelstilskin", 20)
+ * test_example("Rumpelstilskin", 40)
+
+def custom_parametrization(param_names, param_value_defs):
+    param_values = []
+    for uno, due_values in param_value_defs:
+        for due in due_values:
+            param_values.append((uno, due))
+    return (param_names, param_values), {}
+
+ at pytest.indirect_parametrize(custom_parametrization, ("uno", "due"), (
+    ("Fritula", (1, 2, 3, 4, 5, 6, 7, 8, 9)),
+    ("Madagascar", (10, 30, 50)),
+    ("Rumpelstilskin", (20, 40))))
+def test_example(uno, due):
+    assert False
+
+"""
+
+import pytest
+
+
+def indirect_parametrize(func, *args, **kwargs):
+    """
+    Decorator registering a custom parametrization function for a pytest test.
+
+    This pytest.mark.indirect_parametrize() replacement allows the use of
+    indirect parametrization functions taking no input parameters or, with
+    pytest versions prior to 2.5.2, functions taking only keyword arguments.
+
+    If a pytest.mark.indirect_parametrize() call is made with such an indirect
+    parametrization function, it decorates the given function instead of
+    storing and using it to decorate the intended function later on.
+
+    """
+    # In pytest versions prior to 2.5.2 pytest.mark.indirect_parametrize()
+    # special handling occurs even when passing it additional keyword arguments
+    # so we have to make sure we are passing it at least one additional
+    # positional argument.
+    def wrapper(func, *args, **kwargs):
+        return func(*args, **kwargs)
+    return pytest.mark.indirect_parametrize(wrapper, func, *args, **kwargs)
+
+
+def pytest_configure(config):
+    """Describe the new pytest marker in the --markers output."""
+    config.addinivalue_line("markers",
+        "indirect_parametrize(function, argnames, argvalues): similar to the "
+        "builtin pytest.mark.parametrize implementation, except that it takes "
+        "an indirect parametrization function as an additional initial "
+        "positional argument. All the other parameters are forwarded to the "
+        "indirect parametrization function which then returns the actual "
+        "metafunc.parametrize() parameters (standard Python positional "
+        "argument list and keyword argument dictionary) based on the received "
+        "input data. For more detailed information see the "
+        "indirect_parametrize pytest plugin implementation module.")
+
+
+def pytest_generate_tests(metafunc):
+    """pytest hook called for all detected test functions."""
+    func = metafunc.function
+    try:
+        mark = func.indirect_parametrize
+    except AttributeError:
+        return
+    args, kwargs = mark.args[0](*mark.args[1:], **mark.kwargs)
+    metafunc.parametrize(*args, **kwargs)
+
+
+def pytest_namespace():
+    """pytest hook publishing references in the toplevel pytest namespace."""
+    return {'indirect_parametrize': indirect_parametrize}
diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py
new file mode 100644
index 0000000..6afb205
--- /dev/null
+++ b/tests/test_argument_parser.py
@@ -0,0 +1,560 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library web service operation argument parser related unit tests.
+
+Suds library prepares web service operation invocation functions that construct
+actual web service operation invocation requests based on the parameters they
+receive and their web service operation's definition.
+
+The module tested here implements generic argument parsing and validation, not
+specific to a particular web service operation binding.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.argparser
+import tests
+
+import pytest
+
+
+class MockAncestor:
+    """
+    Represents a web service operation parameter ancestry item.
+
+    Implements parts of the suds library's web service operation ancestry item
+    interface required by the argument parser functionality.
+
+    """
+
+    def __init__(self, is_choice=False):
+        self.__is_choice = is_choice
+
+    def choice(self):
+        return self.__is_choice
+
+
+class MockParamProcessor:
+    """
+    Mock parameter processor that gets passed argument parsing results.
+
+    Collects received parameter information so it may be checked after argument
+    parsing has completed.
+
+    """
+
+    def __init__(self):
+        self.params_ = []
+
+    def params(self):
+        return self.params_
+
+    def process(self, param_name, param_type, in_choice_context, value):
+        self.params_.append((param_name, param_type, in_choice_context, value))
+
+
+class MockParamType:
+    """
+    Represents a web service operation parameter type.
+
+    Implements parts of the suds library's web service operation parameter type
+    interface required by the argument parsing implementation tested in this
+    module.
+
+    """
+
+    def __init__(self, optional):
+        self.optional_ = optional
+
+    def optional(self):
+        return self.optional_
+
+
+ at pytest.mark.parametrize("binding_style", (
+    "document",
+    #TODO: Suds library's RPC binding implementation should be updated to use
+    # the argument parsing functionality. This will remove code duplication
+    # between different binding implementations and make their features more
+    # balanced.
+    pytest.mark.xfail(reason="Not yet implemented.")("rpc")
+    ))
+def test_binding_uses_argument_parsing(monkeypatch, binding_style):
+    """
+    Calling web service operations should use the generic argument parsing
+    functionality independent of the operation's specific binding style.
+
+    """
+    class MyException(Exception):
+        pass
+    def raise_exception(*args, **kwargs):
+        raise MyException
+    monkeypatch.setattr(suds.argparser._ArgParser, "__init__", raise_exception)
+
+    wsdl = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Bongo" type="xsd:string" />
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">"
+    <wsdl:part name="parameters" element="ns:Bongo" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="%s" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""" % (binding_style,))
+    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    pytest.raises(MyException, client.service.f)
+    pytest.raises(MyException, client.service.f, "x")
+    pytest.raises(MyException, client.service.f, "x", "y")
+
+
+ at pytest.mark.parametrize(("param_optional", "args"), (
+    # Operations taking no parameters.
+    ((), (1,)),
+    ((), (1, 2)),
+    ((), (1, 2, None)),
+    # Operations taking a single parameter.
+    ((True,), (1, 2)),
+    ((False,), (1, 2)),
+    ((True,), ("2", 2, None)),
+    ((False,), ("2", 2, None)),
+    ((True,),  (object(), 2, None, None)),
+    ((False,), (object(), 2, None, None)),
+    ((True,), (None, 2, None, None, "5")),
+    ((False,), (None, 2, None, None, "5")),
+    # Operations taking multiple parameters.
+    ((True, True), (1, 2, 3)),
+    ((False, True), (1, 2, 3)),
+    ((True, False), (1, 2, 3)),
+    ((False, False), (1, 2, 3)),
+    ((False, True), ("2", 2, None)),
+    ((False, False), ("2", 2, None)),
+    ((True, True), ("2", 2, None)),
+    ((True, True, True), (object(), 2, None, None)),
+    ((False, False, False), (object(), 2, None, None)),
+    ((True, False, False), (None, 2, None, None, "5")),
+    ((True, False, True), (None, 2, None, None, "5")),
+    ((True, True, True), (None, 2, None, None, "5"))))
+def test_extra_positional_arguments(param_optional, args):
+    """
+    Test passing extra positional arguments for an operation expecting more
+    than one.
+
+    """
+    param_count = len(param_optional)
+    params = []
+    expected_args_min = 0
+    for i, optional in enumerate(param_optional):
+        if not optional:
+            expected_args_min += 1
+        param_name = "p%d" % (i,)
+        param_type = MockParamType(optional)
+        params.append((param_name, param_type))
+    param_processor = MockParamProcessor()
+
+    takes_plural_suffix = "s"
+    if expected_args_min == param_count:
+        takes = param_count
+        if param_count == 1:
+            takes_plural_suffix = ""
+    else:
+        takes = "%d to %d" % (expected_args_min, param_count)
+    was_were = "were"
+    if len(args) == 1:
+        was_were = "was"
+    expected = "fru-fru() takes %s positional argument%s but %d %s given" % (
+        takes, takes_plural_suffix, len(args), was_were)
+    _expect_error(TypeError, expected, suds.argparser.parse_args, "fru-fru",
+        params, args, {}, param_processor.process, True)
+
+    assert len(param_processor.params()) == param_count
+    processed_params = param_processor.params()
+    for expected_param, param, value in zip(params, processed_params, args):
+        assert param[0] is expected_param[0]
+        assert param[1] is expected_param[1]
+        assert not param[2]
+        assert param[3] is value
+
+
+ at pytest.mark.parametrize(("param_names", "args", "kwargs"), (
+    (["a"], (1,), {"a":5}),
+    ([["a"]], (1,), {"a":5}),
+    (["a"], (None, 1, 2, 7), {"a":5}),
+    ([["a"]], (None, 1, 2, 7), {"a":5}),
+    (["a", ["b"], "c"], (None, None, None), {"a":1, "b":2, "c":3}),
+    ([["a"], ["b"], ["c"]], (None, None, None), {"a":1, "b":2, "c":3}),
+    (["a"], ("x",), {"a":None}),
+    (["a", ["b"], ["c"]], (1,), {"a":None}),
+    (["a", "b", ["c"]], (None, 2), {"b":None})))
+def test_multiple_value_for_single_parameter_error(param_names, args, kwargs):
+    """
+    Test how multiple value for a single parameter errors are reported.
+
+    This report takes precedence over any extra positional argument errors.
+
+    Optional parameters are marked by specifying their names as single element
+    lists or tuples.
+
+    """
+    params = []
+    duplicates = []
+    args_count = len(args)
+    for n, param_name in enumerate(param_names):
+        optional = False
+        if param_name.__class__ in (tuple, list):
+            optional = True
+            param_name = param_name[0]
+        if n < args_count and param_name in kwargs:
+            duplicates.append(param_name)
+        params.append((param_name, MockParamType(optional)))
+    message = "q() got multiple values for parameter '%s'"
+    expected = [message % (x,) for x in duplicates]
+    if len(expected) == 1:
+        expected = expected[0]
+    _expect_error(TypeError, expected, suds.argparser.parse_args, "q", params,
+        args, kwargs, _do_nothing, True)
+
+
+def test_not_reporting_extra_argument_errors():
+    """
+    When ArgParser has been configured not to report extra argument errors as
+    exceptions, it should simply ignore any such extra arguments. This matches
+    the suds library behaviour from before extra argument error reporting was
+    implemented.
+
+    """
+    x = MockAncestor()
+    c = MockAncestor(is_choice=True)
+    params = [
+        ("p1", MockParamType(False), [x]),
+        ("p2", MockParamType(True), [x, c]),
+        ("p3", MockParamType(False), [x, c])]
+    args = list(range(5))
+    kwargs = {"p1":"p1", "p3":"p3", "x":666}
+    param_processor = MockParamProcessor()
+    args_required, args_allowed = suds.argparser.parse_args("w", params, args,
+        kwargs, param_processor.process, False)
+
+    assert args_required == 1
+    assert args_allowed == 3
+    processed_params = param_processor.params()
+    assert len(processed_params) == len(params)
+    for expected_param, param, value in zip(params, processed_params, args):
+        assert param[0] is expected_param[0]
+        assert param[1] is expected_param[1]
+        assert param[2] == (c in expected_param[2])
+        assert param[3] is value
+
+
+ at pytest.mark.parametrize(("param_names", "args", "kwargs"), (
+    ([], (), {"x":5}),
+    ([], (None, 1, 2, 7), {"x":5}),
+    ([], (), {"x":1, "y":2, "z":3}),
+    (["a"], (), {"x":None}),
+    ([["a"]], (), {"x":None}),
+    (["a"], (1,), {"x":None}),
+    ([["a"]], (1,), {"x":None}),
+    (["a"], (), {"a":"spank me", "x":5}),
+    (["a"], (), {"x":5, "a":"spank me"}),
+    (["a"], (), {"a":"spank me", "x":5, "wuwu":None}),
+    (["a", "b", "c"], (1, 2), {"w":666}),
+    (["a", ["b"], ["c"]], (1,), {"c":None, "w":666}),
+    (["a", "b", ["c"]], (None,), {"b":None, "_":666})))
+def test_unexpected_keyword_argument(param_names, args, kwargs):
+    """
+    Test how unexpected keyword arguments are reported.
+
+    This report takes precedence over any extra positional argument errors.
+
+    Optional parameters are marked by specifying their names as single element
+    lists or tuples.
+
+    """
+    params = []
+    arg_count = len(args)
+    for n, param_name in enumerate(param_names):
+        optional = False
+        if param_name.__class__ in (tuple, list):
+            optional = True
+            param_name = param_name[0]
+        if n < arg_count:
+            assert param_name not in kwargs
+        else:
+            kwargs.pop(param_name, None)
+        params.append((param_name, MockParamType(optional)))
+    message = "pUFf() got an unexpected keyword argument '%s'"
+    expected = [message % (x,) for x in kwargs]
+    if len(expected) == 1:
+        expected = expected[0]
+    _expect_error(TypeError, expected, suds.argparser.parse_args, "pUFf",
+        params, args, kwargs, _do_nothing, True)
+
+
+ at pytest.mark.parametrize(("expect_required", "expect_allowed", "param_defs"), (
+    # No parameters.
+    (0, 0, []),
+    # Single parameter.
+    (1, 1, [("p1", False, [1, 2, 3, 4])]),
+    (0, 1, [("p1", True, [1, 2, 3, 4])]),
+    (1, 1, [("p1", False, [1, 2, 3, [4]])]),
+    (0, 1, [("p1", True, [1, 2, 3, [4]])]),
+    (1, 1, [("p1", False, [1, [2], 3, 4])]),
+    (0, 1, [("p1", True, [1, [2], 3, 4])]),
+    (1, 1, [("p1", False, [1, [2], 3, [4]])]),
+    (0, 1, [("p1", True, [1, [2], 3, [4]])]),
+    # Multiple parameters.
+    (4, 4, [
+        ("a", False, [1]),
+        ("b", False, [1]),
+        ("c", False, [1]),
+        ("d", False, [1])]),
+    (0, 4, [
+        ("a", True, [1]),
+        ("b", True, [1]),
+        ("c", True, [1]),
+        ("d", True, [1])]),
+    (2, 4, [
+        ("a", True, [1]),
+        ("b", False, [1]),
+        ("c", True, [1]),
+        ("d", False, [1])]),
+    (2, 4, [
+        ("a", False, [1]),
+        ("b", True, [1]),
+        ("c", False, [1]),
+        ("d", True, [1])]),
+    (3, 4, [
+        ("a", False, [1]),
+        ("b", False, [1]),
+        ("c", False, [1]),
+        ("d", True, [1])]),
+    (3, 4, [
+        ("a", True, [1]),
+        ("b", False, [1]),
+        ("c", False, [1]),
+        ("d", False, [1])]),
+    # Choice containing only simple members.
+    (1, 2, [
+        ("a", False, [[1]]),
+        ("b", False, [[1]])]),
+    (0, 2, [
+        ("a", True, [[1]]),
+        ("b", False, [[1]])]),
+    (0, 2, [
+        ("a", False, [[1]]),
+        ("b", True, [[1]])]),
+    (0, 2, [
+        ("a", True, [[1]]),
+        ("b", True, [[1]])]),
+    # Choice containing a non-empty sequence.
+    (1, 3, [
+        ("a", False, [1, 2, 3, [4]]),
+        ("b1", False, [1, 2, 3, [4], 5]),
+        ("b2", False, [1, 2, 3, [4], 5])]),
+    # Choice with more than one required parameter.
+    (2, 4, [
+        ("a1", False, [[1], 2]),
+        ("a2", False, [[1], 2]),
+        ("b1", False, [[1], 3]),
+        ("b2", False, [[1], 3])]),
+    (2, 5, [
+        ("a1", False, [[1], 2]),
+        ("a2", False, [[1], 2]),
+        ("b1", False, [[1], 3]),
+        ("b2", False, [[1], 3]),
+        ("b3", False, [[1], 3])]),
+    (2, 5, [
+        ("a1", False, [[1], 2]),
+        ("a2", False, [[1], 2]),
+        ("a3", False, [[1], 2]),
+        ("b1", False, [[1], 3]),
+        ("b2", False, [[1], 3])]),
+    (3, 6, [
+        ("a1", False, [[1], 2]),
+        ("a2", False, [[1], 2]),
+        ("a3", False, [[1], 2]),
+        ("b1", False, [[1], 3]),
+        ("b2", False, [[1], 3]),
+        ("b3", False, [[1], 3])]),
+    (2, 6, [
+        ("a1", False, [[1], 2]),
+        ("a2", True, [[1], 2]),
+        ("a3", False, [[1], 2]),
+        ("b1", False, [[1], 3]),
+        ("b2", False, [[1], 3]),
+        ("b3", False, [[1], 3])]),
+    # Sequence containing multiple choices.
+    (2, 4, [
+        ("a1", False, [0, [1]]),
+        ("a2", False, [0, [1]]),
+        ("b1", False, [0, [2]]),
+        ("b2", False, [0, [2]])]),
+    (1, 4, [
+        ("a1", False, [0, [1]]),
+        ("a2", False, [0, [1]]),
+        ("b1", False, [0, [2]]),
+        ("b2", True, [0, [2]])]),
+    (3, 5, [
+        ("a1", False, [0, [1]]),
+        ("a2", False, [0, [1]]),
+        ("x", False, [0]),
+        ("b1", False, [0, [2]]),
+        ("b2", False, [0, [2]])]),
+    # Choice containing optional parameters.
+    (0, 3, [
+        ("a", False, [1, [2]]),
+        ("b", True, [1, [2]]),
+        ("c", False, [1, [2]])]),
+    (0, 3, [
+        ("a", False, [1, [2]]),
+        ("b1", True, [1, [2], 3]),
+        ("b2", True, [1, [2], 3])]),
+    (1, 3, [
+        ("a", False, [1, [2]]),
+        ("b1", False, [1, [2], 3]),
+        ("b2", True, [1, [2], 3])]),
+    # Choices within choices next to choices.
+    (3, 14, [
+        ("p01", False, [1]),
+        ("p02", False, [1, [2], 3]),
+        ("p03", False, [1, [2], 3]),
+        ("p04", False, [1, [2], 4, 5, 6]),
+        ("p05", False, [1, [2], 4, 5, 6, [7]]),
+        ("p06", False, [1, [2], 4, 5, 6, [7], [8]]),
+        ("p07", False, [1, [2], 4, 5, 6, [7], 9]),
+        ("p08", False, [1, [2], 4, 5, 6, [7], 9]),
+        ("p09", False, [1, [2], 4, [10], 11]),
+        ("p10", False, [1, [2], 4, [10], 11]),
+        ("p11", False, [1, [2], 4, [10], 12]),
+        ("p12", False, [1, [2], 4, [10], 12]),
+        ("p13", False, [1, [2], 4, [10], 12]),
+        ("p14", False, [1, [2], 4, [13]])]),
+    ))
+def test_unwrapped_arg_counts(expect_required, expect_allowed, param_defs):
+    """
+    Test required & allowed argument count for unwrapped parameters.
+
+    Expected 'param_defs' structure - list of 3-tuples containing the
+    following:
+      * Parameter name (string).
+      * Optional (boolean).
+      * Ancestry (list).
+        * Contains integers and/or single element lists containing an integer.
+          * Integers represent non-choice ancestry items.
+          * Single element lists represent choice ancestry items.
+        * Integer values represent ancestry item ids - different integer values
+          represent separate ancestry items.
+
+    """
+    ancestor_map = {}
+    params = []
+    for param_name, param_optional, param_ancestry_def in param_defs:
+        ancestry = []
+        for n, id in enumerate(param_ancestry_def):
+            is_choice = False
+            if id.__class__ is list:
+                assert len(id) == 1, "bad test input"
+                id = id[0]
+                is_choice = True
+            try:
+                ancestor, ancestry_def = ancestor_map[id]
+            except KeyError:
+                ancestor = MockAncestor(is_choice)
+                ancestor_map[id] = (ancestor, param_ancestry_def[:n])
+            else:
+                assert ancestor.choice() == is_choice, "bad test input"
+                assert ancestry_def == param_ancestry_def[:n], "bad test input"
+            ancestry.append(ancestor)
+        params.append((param_name, MockParamType(param_optional), ancestry))
+    param_processor = MockParamProcessor()
+    args = [object() for x in params]
+    args_required, args_allowed = suds.argparser.parse_args("w", params, args,
+        {}, param_processor.process, False)
+
+    assert args_required == expect_required
+    assert args_allowed == expect_allowed
+    processed_params = param_processor.params()
+    assert len(processed_params) == len(params)
+    for expected_param, param, value in zip(params, processed_params, args):
+        assert param[0] is expected_param[0]
+        assert param[1] is expected_param[1]
+        expected_in_choice_context = False
+        for x in expected_param[2]:
+            if x.choice():
+                expected_in_choice_context = True
+                break
+        assert param[2] == expected_in_choice_context
+        assert param[3] is value
+
+
+def _do_nothing(*args, **kwargs):
+    """Do-nothing function used as a callback where needed during testing."""
+    pass
+
+
+def _expect_error(expected_exception, expected_error_text, test_function,
+        *args, **kwargs):
+    """
+    Assert a test function call raises an expected exception.
+
+    Caught exception is considered expected if its string representation
+    matches the given expected error text.
+
+    Expected error text may be given directly or as a list/tuple containing
+    valid alternatives.
+
+    """
+    e = pytest.raises(expected_exception, test_function, *args, **kwargs).value
+    try:
+        if expected_error_text.__class__ in (list, tuple):
+            assert str(e) in expected_error_text
+        else:
+            assert str(e) == expected_error_text
+    finally:
+        del e
diff --git a/tests/test_cache.py b/tests/test_cache.py
new file mode 100644
index 0000000..2044c96
--- /dev/null
+++ b/tests/test_cache.py
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library document caching unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.cache
+import suds.sax.parser
+
+import pytest
+
+import os
+import tempfile
+
+
+class InvisibleMan:
+    """Dummy class used for pickling related tests."""
+    def __init__(self, x):
+        self.x = x
+
+
+# Hardcoded values used in different caching test cases.
+value_empty = suds.byte_str("")
+value_f2 = suds.byte_str("fifi2")
+value_f22 = suds.byte_str("fifi22")
+value_f3 = suds.byte_str("fifi3")
+value_p1 = suds.byte_str("pero1")
+value_p11 = suds.byte_str("pero11")
+value_p111 = suds.byte_str("pero111")
+value_p2 = suds.byte_str("pero2")
+value_p22 = suds.byte_str("pero22")
+value_unicode = suds.byte_str(u"€ 的 čćžšđČĆŽŠĐ")
+
+
+def test_Cache():
+    cache = suds.cache.Cache()
+    pytest.raises(Exception, cache.get, "id")
+    pytest.raises(Exception, cache.put, "id", "object")
+    pytest.raises(Exception, cache.purge, "id")
+    pytest.raises(Exception, cache.clear)
+
+
+def test_DocumentCache(tmpdir):
+    cacheFolder = tmpdir.join("puffy").strpath
+    cache = suds.cache.DocumentCache(cacheFolder)
+    assert isinstance(cache, suds.cache.FileCache)
+    assert cache.get("unga1") is None
+
+    # TODO: DocumentCache class interface seems silly. Its get() operation
+    # returns an XML document while its put() operation takes an XML element.
+    # The put() operation also silently ignores passed data of incorrect type.
+    # TODO: Update this test to no longer depend on the exact input XML data
+    # formatting. We currently expect it to be formatted exactly as what gets
+    # read back from the DocumentCache.
+    content = suds.byte_str("""\
+<xsd:element name="Elemento">
+   <xsd:simpleType>
+      <xsd:restriction base="xsd:string">
+         <xsd:enumeration value="alfa"/>
+         <xsd:enumeration value="beta"/>
+         <xsd:enumeration value="gamma"/>
+      </xsd:restriction>
+   </xsd:simpleType>
+</xsd:element>""")
+    xml = suds.sax.parser.Parser().parse(suds.BytesIO(content))
+    cache.put("unga1", xml.getChildren()[0])
+    readXML = cache.get("unga1")
+    assert isinstance(readXML, suds.sax.document.Document)
+    readXMLElements = readXML.getChildren()
+    assert len(readXMLElements) == 1
+    readXMLElement = readXMLElements[0]
+    assert isinstance(readXMLElement, suds.sax.element.Element)
+    assert suds.byte_str(str(readXMLElement)) == content
+
+
+def test_FileCache():
+    cache = suds.cache.FileCache()
+    assert isinstance(cache, suds.cache.Cache)
+
+
+def test_FileCache_clear(tmpdir):
+    cacheFolder1 = tmpdir.join("fungus").strpath
+    cache1 = suds.cache.FileCache(cacheFolder1)
+    cache1.put("unga1", value_p1)
+    cache1.put("unga2", value_p2)
+    assert cache1.get("unga1") == value_p1
+    assert cache1.get("unga2") == value_p2
+    cache1.clear()
+    assert _isEmptyCacheFolder(cacheFolder1)
+    assert cache1.get("unga1") is None
+    assert cache1.get("unga2") is None
+    cache1.put("unga1", value_p11)
+    cache1.put("unga2", value_p2)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+
+    cacheFolder2 = tmpdir.join("broccoli").strpath
+    cache2 = suds.cache.FileCache(cacheFolder2)
+    cache2.put("unga2", value_f2)
+    assert cache2.get("unga2") == value_f2
+    cache2.clear()
+    assert not _isEmptyCacheFolder(cacheFolder1)
+    assert _isEmptyCacheFolder(cacheFolder2)
+    assert cache2.get("unga2") is None
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+    cache2.put("unga2", value_p22)
+    assert cache2.get("unga2") == value_p22
+
+
+def test_FileCache_location(tmpdir):
+    defaultLocation = os.path.join(tempfile.gettempdir(), "suds")
+    cache = suds.cache.FileCache()
+    assert os.path.isdir(cache.location)
+    assert cache.location == defaultLocation
+    assert suds.cache.FileCache().location == defaultLocation
+    assert cache.location == defaultLocation
+
+    cacheFolder1 = tmpdir.join("flip-flop1").strpath
+    assert not os.path.isdir(cacheFolder1)
+    assert suds.cache.FileCache(location=cacheFolder1).location == cacheFolder1
+    assert _isEmptyCacheFolder(cacheFolder1)
+
+    cacheFolder2 = tmpdir.join("flip-flop2").strpath
+    assert not os.path.isdir(cacheFolder2)
+    assert suds.cache.FileCache(cacheFolder2).location == cacheFolder2
+    assert _isEmptyCacheFolder(cacheFolder2)
+
+
+def test_FileCache_close_leaves_cached_files_behind(tmpdir):
+    cacheFolder1 = tmpdir.join("ana").strpath
+    cache1 = suds.cache.FileCache(cacheFolder1)
+    cache1.put("unga1", value_p1)
+    cache1.put("unga2", value_p2)
+
+    cacheFolder2 = tmpdir.join("nan").strpath
+    cache2 = suds.cache.FileCache(cacheFolder2)
+    cache2.put("unga2", value_f2)
+    cache2.put("unga3", value_f3)
+
+    del cache1
+
+    cache11 = suds.cache.FileCache(cacheFolder1)
+    assert cache11.get("unga1") == value_p1
+    assert cache11.get("unga2") == value_p2
+    assert cache2.get("unga2") == value_f2
+    assert cache2.get("unga3") == value_f3
+
+
+def test_FileCache_get_put(tmpdir):
+    cacheFolder1 = tmpdir.join("firefly").strpath
+    cache1 = suds.cache.FileCache(cacheFolder1)
+    assert _isEmptyCacheFolder(cacheFolder1)
+    assert cache1.get("unga1") is None
+    cache1.put("unga1", value_p1)
+    assert not _isEmptyCacheFolder(cacheFolder1)
+    assert cache1.get("unga1") == value_p1
+    assert cache1.get("unga2") is None
+    cache1.put("unga1", value_p11)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") is None
+    cache1.put("unga2", value_p2)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+
+    cacheFolder2 = tmpdir.join("semper fi").strpath
+    cache2 = suds.cache.FileCache(cacheFolder2)
+    assert _isEmptyCacheFolder(cacheFolder2)
+    assert cache2.get("unga2") is None
+    cache2.put("unga2", value_f2)
+    assert not _isEmptyCacheFolder(cacheFolder2)
+    assert cache2.get("unga2") == value_f2
+    assert cache2.get("unga3") is None
+    cache2.put("unga2", value_f22)
+    assert cache2.get("unga2") == value_f22
+    assert cache2.get("unga3") is None
+    cache2.put("unga3", value_f3)
+    assert cache2.get("unga2") == value_f22
+    assert cache2.get("unga3") == value_f3
+
+    assert not _isEmptyCacheFolder(cacheFolder1)
+    assert not _isEmptyCacheFolder(cacheFolder2)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+    assert cache1.get("unga3") is None
+    assert cache2.get("unga1") is None
+    assert cache2.get("unga2") == value_f22
+    assert cache2.get("unga3") == value_f3
+
+
+def test_FileCache_purge(tmpdir):
+    cacheFolder1 = tmpdir.join("flamenco").strpath
+    cache1 = suds.cache.FileCache(cacheFolder1)
+    cache1.put("unga1", value_p1)
+    assert cache1.get("unga1") == value_p1
+    cache1.purge("unga1")
+    assert _isEmptyCacheFolder(cacheFolder1)
+    assert cache1.get("unga1") is None
+    cache1.put("unga1", value_p11)
+    cache1.put("unga2", value_p2)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+    cache1.purge("unga1")
+    assert cache1.get("unga1") is None
+    assert cache1.get("unga2") == value_p2
+    cache1.put("unga1", value_p111)
+
+    cacheFolder2 = tmpdir.join("shadow").strpath
+    cache2 = suds.cache.FileCache(cacheFolder2)
+    cache2.put("unga2", value_f2)
+    cache2.purge("unga2")
+    assert _isEmptyCacheFolder(cacheFolder2)
+    assert cache1.get("unga1") == value_p111
+    assert cache1.get("unga2") == value_p2
+    assert cache2.get("unga2") is None
+
+
+def test_FileCache_reused_cache_folder(tmpdir):
+    cacheFolder = tmpdir.strpath
+    cache1 = suds.cache.FileCache(cacheFolder)
+    assert _isEmptyCacheFolder(cacheFolder)
+    assert cache1.get("unga1") is None
+    cache1.put("unga1", value_p1)
+    assert cache1.get("unga1") == value_p1
+    assert cache1.get("unga2") is None
+    cache1.put("unga1", value_p11)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") is None
+    cache1.put("unga2", value_p2)
+    assert cache1.get("unga1") == value_p11
+    assert cache1.get("unga2") == value_p2
+
+    cache2 = suds.cache.FileCache(cacheFolder)
+    assert cache2.get("unga1") == value_p11
+    assert cache2.get("unga2") == value_p2
+    cache2.put("unga3", value_f3)
+    assert cache1.get("unga3") == value_f3
+
+
+def test_FileCache_version(tmpdir):
+    fakeVersionInfo = "--- fake version info ---"
+    assert suds.__version__ != fakeVersionInfo
+
+    cacheFolder = tmpdir.join("hitori")
+    versionFile = cacheFolder.join("version")
+    cache = suds.cache.FileCache(cacheFolder.strpath)
+    assert versionFile.read() == suds.__version__
+    cache.put("unga1", value_p1)
+
+    versionFile.write(fakeVersionInfo)
+    assert cache.get("unga1") == value_p1
+
+    cache2 = suds.cache.FileCache(cacheFolder.strpath)
+    assert _isEmptyCacheFolder(cacheFolder.strpath)
+    assert cache.get("unga1") is None
+    assert cache2.get("unga1") is None
+    assert versionFile.read() == suds.__version__
+    cache.put("unga1", value_p11)
+    cache.put("unga2", value_p22)
+
+    versionFile.remove()
+    assert cache.get("unga1") == value_p11
+    assert cache.get("unga2") == value_p22
+
+    cache3 = suds.cache.FileCache(cacheFolder.strpath)
+    assert _isEmptyCacheFolder(cacheFolder.strpath)
+    assert cache.get("unga1") is None
+    assert cache.get("unga2") is None
+    assert cache2.get("unga1") is None
+    assert versionFile.read() == suds.__version__
+
+
+def test_FileCache_with_empty_cached_content(tmpdir):
+    cacheFolder = tmpdir.strpath
+    cache = suds.cache.FileCache(cacheFolder)
+    cache.put("unga1", value_empty)
+    assert cache.get("unga1") == value_empty
+    assert not _isEmptyCacheFolder(cacheFolder)
+
+
+def test_FileCache_with_random_utf_character_cached_content(tmpdir):
+    cacheFolder = tmpdir.strpath
+    cache = suds.cache.FileCache(cacheFolder)
+    cache.put("unga1", value_unicode)
+    assert cache.get("unga1") == value_unicode
+    assert not _isEmptyCacheFolder(cacheFolder)
+
+
+def test_NoCache():
+    cache = suds.cache.NoCache()
+    assert isinstance(cache, suds.cache.Cache)
+
+    assert cache.get("id") == None
+    cache.put("id", "something")
+    assert cache.get("id") == None
+
+    # TODO: It should not be an error to call purge() or clear() on a NoCache
+    # instance.
+    pytest.raises(Exception, cache.purge, "id")
+    pytest.raises(Exception, cache.clear)
+
+
+def test_ObjectCache(tmpdir):
+    cacheFolder = tmpdir.join("george carlin").strpath
+    cache = suds.cache.ObjectCache(cacheFolder)
+    assert isinstance(cache, suds.cache.FileCache)
+    assert cache.get("unga1") is None
+    assert cache.get("unga2") is None
+    cache.put("unga1", InvisibleMan(1))
+    cache.put("unga2", InvisibleMan(2))
+    read1 = cache.get("unga1")
+    read2 = cache.get("unga2")
+    assert read1.__class__ is InvisibleMan
+    assert read2.__class__ is InvisibleMan
+    assert read1.x == 1
+    assert read2.x == 2
+
+
+def _isEmptyCacheFolder(folder):
+    assert os.path.isdir(folder)
+    def walkError(error):
+        pytest.fail("Error attempting to walk through cache folder contents.")
+    count = 0
+    for root, folders, files in os.walk(folder, onerror=walkError):
+        assert root == folder
+        return len(folders) == 0 and len(files) == 1 and files[0] == 'version'
+    return False
diff --git a/tests/test_client_cache.py b/tests/test_client_cache.py
new file mode 100644
index 0000000..0270bf1
--- /dev/null
+++ b/tests/test_client_cache.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library client cache related unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.cache
+import suds.store
+
+import pytest
+
+
+class MyException(Exception):
+    """Local exception class used in this testing module."""
+    pass
+
+
+def test_default_cache_construction(monkeypatch):
+    """
+    Test when and how client creates its default cache object.
+
+    We use a dummy store to get an expected exception rather than attempting to
+    access the network, in case the test fails and the expected default cache
+    object does not get created or gets created too late.
+
+    """
+    def constructDefaultCache(days):
+        assert days == 1
+        raise MyException
+    class MockStore(suds.store.DocumentStore):
+        def open(self, *args, **kwargs):
+            pytest.fail("Default cache not created in time.")
+    monkeypatch.setattr("suds.client.ObjectCache", constructDefaultCache)
+    monkeypatch.setattr("suds.store.DocumentStore", MockStore)
+    pytest.raises(MyException, suds.client.Client, "some_url",
+        documentStore=MockStore())
+
+
+ at pytest.mark.parametrize("cache", (
+    None,
+    suds.cache.NoCache(),
+    suds.cache.ObjectCache()))
+def test_avoiding_default_cache(cache, monkeypatch):
+    """Explicitly specified cache should avoid default cache construction."""
+    def constructDefaultCache(*args, **kwargs):
+        pytest.fail("Unexpected default cache instantiation.")
+    class MockStore(suds.store.DocumentStore):
+        def open(self, *args, **kwargs):
+            raise MyException
+    monkeypatch.setattr("suds.client.ObjectCache", constructDefaultCache)
+    monkeypatch.setattr("suds.store.DocumentStore", MockStore)
+    pytest.raises(MyException, suds.client.Client, "some_url",
+        documentStore=MockStore(), cache=cache)
diff --git a/tests/test_date_time.py b/tests/test_date_time.py
new file mode 100644
index 0000000..dc55a71
--- /dev/null
+++ b/tests/test_date_time.py
@@ -0,0 +1,463 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jeff Ortel ( jortel at redhat.com )
+
+"""
+Date & time related suds Python library unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+from suds.sax.date import (FixedOffsetTimezone, Date, DateTime, Time,
+    UtcTimezone)
+from suds.xsd.sxbuiltin import XDate, XDateTime, XTime
+import tests
+
+import pytest
+
+import datetime
+
+
+class _Dummy:
+    """Class for testing unknown object class handling."""
+    pass
+
+
+"""Invalid date strings reused for both date & datetime testing."""
+_invalid_date_strings = (
+    "",
+    "abla",
+    "12",
+    "12-01",
+    "-12-01",
+    "1900-01",
+    "+1900-10-01",  # Plus sign not allowed.
+    "1900-13-01",  # Invalid month.
+    "1900-02-30",  # Invalid day.
+    "2001-02-29",  # Not a leap year.
+    "2100-02-29",  # Not a leap year.
+    " 1900-01-01",
+    "1900- 01-01",
+    "1900-01 -01",
+    "1900-01-01 ",
+    "1900-13-011",
+    "1900-01-01X",
+    "1900-01-01T",  # 'T' is a date/time separator for DateTime.
+    # Invalid time zone indicators.
+        "1900-01-01 +17:00",
+        "1900-01-01+ 17:00",
+        "1900-01-01*17:00",
+        "1900-01-01 17:00",
+        "1900-01-01+17:",
+        "1900-01-01+170",
+        "1900-01-01+1730",
+        "1900-01-01+170:00",
+        "1900-01-01+17:00:00",
+        "1900-01-01-:4",
+        "1900-01-01-2a:00",
+        "1900-01-01-222:00",
+        "1900-01-01-12:000"
+        "1900-01-01+00:60",
+        "1900-01-01-00:99")
+
+"""Invalid date strings reused for both time & datetime testing."""
+_invalid_time_strings = (
+    "",
+    "bunga",
+    "12",
+    "::",
+    "12:",
+    "12:01",
+    "12:01:",
+    "12:01: 00",
+    "12:01:  00",
+    "23: 01:00",
+    " 23:01:00",
+    "23 :01:00",
+    "23::00",
+    "23:000:00",
+    "023:00:00",
+    "23:00:000",
+    "25:01:00",
+    "-1:01:00",
+    "24:01:00",
+    "23:-1:00",
+    "23:61:00",
+    "23:60:00",
+    "23:59:-1",
+    "23:59:61",
+    "23:59:60",
+    "7.59.13",
+    "7-59-13",
+    "-0:01:00",
+    "23:-0:00",
+    "23:59:-0",
+    "23:59:6.a",
+    "23:59:6.",
+    "23:59:6:0",
+    "23:59:6.12x",
+    "23:59:6.12x45",
+    "23:59:6.999999 ",
+    "23:59:6.999999x",
+    "T23:59:6",
+    # Invalid time zone indicators.
+        "13:27:04 -10:00",
+        "13:27:04- 10:00",
+        "13:27:04*17:00",
+        "13:27:04 17:00",
+        "13:27:04-003",
+        "13:27:04-003:00",
+        "13:27:04+00:002",
+        "13:27:04-13:60",
+        "13:27:04-121",
+        "13:27:04-1210",
+        "13:27:04-121:00",
+        "13:27:04+12:",
+        "13:27:04+12:00:00",
+        "13:27:04-:13"
+        "13:27:04-24:00"
+        "13:27:04+99:00")
+
+
+class TestDate:
+    """Tests for the suds.sax.date.Date class."""
+
+    def testConstructFromDate(self):
+        date = datetime.date(2001, 12, 10)
+        assert Date(date).value is date
+
+    def testConstructFromDateTime_naive(self):
+        date = datetime.datetime(2001, 12, 10, 10, 50, 21, 32132)
+        assert Date(date).value == datetime.date(2001, 12, 10)
+
+    @pytest.mark.parametrize("hours", (5, 20))
+    def testConstructFromDateTime_tzAware(self, hours):
+        tz = FixedOffsetTimezone(10)
+        date = datetime.datetime(2001, 12, 10, hours, 50, 21, 32132, tzinfo=tz)
+        assert Date(date).value == datetime.date(2001, 12, 10)
+
+    @pytest.mark.parametrize(("string", "y", "m", "d"), (
+        ("1900-01-01", 1900, 1, 1),
+        ("1900-1-1", 1900, 1, 1),
+        ("1900-01-01z", 1900, 1, 1),
+        ("1900-01-01Z", 1900, 1, 1),
+        ("1900-01-01-02", 1900, 1, 1),
+        ("1900-01-01+2", 1900, 1, 1),
+        ("1900-01-01+02:00", 1900, 1, 1),
+        ("1900-01-01+99:59", 1900, 1, 1),
+        ("1900-01-01-21:13", 1900, 1, 1),
+        ("2000-02-29", 2000, 2, 29)))  # Leap year.
+    def testConstructFromString(self, string, y, m, d):
+        assert Date(string).value == datetime.date(y, m, d)
+
+    @pytest.mark.parametrize("string", _invalid_date_strings)
+    def testConstructFromString_failure(self, string):
+        pytest.raises(ValueError, Date, string)
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.time(10, 10)))
+    def testConstructFromUnknown(self, source):
+        pytest.raises(ValueError, Date, source)
+
+    @pytest.mark.parametrize(("input", "output"), (
+        ("1900-01-01", "1900-01-01"),
+        ("2000-02-29", "2000-02-29"),
+        ("1900-1-1", "1900-01-01"),
+        ("1900-01-01z", "1900-01-01"),
+        ("1900-01-01Z", "1900-01-01"),
+        ("1900-01-01-02", "1900-01-01"),
+        ("1900-01-01+2", "1900-01-01"),
+        ("1900-01-01+02:00", "1900-01-01"),
+        ("1900-01-01+99:59", "1900-01-01"),
+        ("1900-01-01-21:13", "1900-01-01")))
+    def testConvertToString(self, input, output):
+        assert str(Date(input)) == output
+
+
+class TestDateTime:
+    """Tests for the suds.sax.date.DateTime class."""
+
+    def testConstructFromDateTime(self):
+        dt = datetime.datetime(2001, 12, 10, 1, 1)
+        assert DateTime(dt).value is dt
+        dt.replace(tzinfo=UtcTimezone())
+        assert DateTime(dt).value is dt
+
+    @pytest.mark.parametrize(
+        ("string", "y", "M", "d", "h", "m", "s", "micros"), (
+        ("2013-11-19T14:05:23.428068", 2013, 11, 19, 14, 5, 23, 428068),
+        ("2013-11-19 14:05:23.4280", 2013, 11, 19, 14, 5, 23, 428000)))
+    def testConstructFromString(self, string, y, M, d, h, m, s, micros):
+        assert DateTime(string).value == datetime.datetime(y, M, d, h, m, s,
+            micros)
+
+    @pytest.mark.parametrize("string",
+        [x + "T00:00:00" for x in _invalid_date_strings] +
+        ["2000-12-31T" + x for x in _invalid_time_strings] + [
+        # Invalid date/time separator characters.
+            "2013-11-1914:05:23.428068",
+            "2013-11-19X14:05:23.428068"])
+    def testConstructFromString_failure(self, string):
+        pytest.raises(ValueError, DateTime, string)
+
+    @pytest.mark.parametrize(
+        ("string", "y", "M", "d", "h", "m", "s", "micros"), (
+        ("2000-2-28T23:59:59.9999995", 2000, 2, 29, 0, 0, 0, 0),
+        ("2000-2-29T23:59:59.9999995", 2000, 3, 1, 0, 0, 0, 0),
+        ("2013-12-31T23:59:59.9999994", 2013, 12, 31, 23, 59, 59, 999999),
+        ("2013-12-31T23:59:59.99999949", 2013, 12, 31, 23, 59, 59, 999999),
+        ("2013-12-31T23:59:59.9999995", 2014, 1, 1, 0, 0, 0, 0)))
+    def testConstructFromString_subsecondRounding(self, string, y, M, d, h, m,
+        s, micros):
+        ref = datetime.datetime(y, M, d, h, m, s, micros)
+        assert DateTime(string).value == ref
+
+    @pytest.mark.parametrize(
+        ("string", "y", "M", "d", "h", "m", "s", "micros", "tz_h", "tz_m"), (
+        ("2013-11-19T14:05:23.428068-3",
+            2013, 11, 19, 14, 5, 23, 428068, -3, 0),
+        ("2013-11-19T14:05:23.068+03",
+            2013, 11, 19, 14, 5, 23, 68000, 3, 0),
+        ("2013-11-19T14:05:23.428068-02:00",
+            2013, 11, 19, 14, 5, 23, 428068, -2, 0),
+        ("2013-11-19T14:05:23.428068+02:00",
+            2013, 11, 19, 14, 5, 23, 428068, 2, 0),
+        ("2013-11-19T14:05:23.428068-23:59",
+            2013, 11, 19, 14, 5, 23, 428068, -23, -59)))
+    def testConstructFromString_timezone(self, string, y, M, d, h, m, s,
+        micros, tz_h, tz_m):
+        tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
+        tzinfo = FixedOffsetTimezone(tzdelta)
+        ref = datetime.datetime(y, M, d, h, m, s, micros, tzinfo=tzinfo)
+        assert DateTime(string).value == ref
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.date(2010, 10, 27),
+        datetime.time(10, 10)))
+    def testConstructFromUnknown(self, source):
+        pytest.raises(ValueError, DateTime, source)
+
+    @pytest.mark.parametrize(("input", "output"), (
+        ("2013-11-19T14:05:23.428068", "2013-11-19T14:05:23.428068"),
+        ("2013-11-19 14:05:23.4280", "2013-11-19T14:05:23.428000"),
+        ("2013-12-31T23:59:59.9999995", "2014-01-01T00:00:00"),
+        ("2013-11-19T14:05:23.428068-3", "2013-11-19T14:05:23.428068-03:00"),
+        ("2013-11-19T14:05:23.068+03", "2013-11-19T14:05:23.068000+03:00"),
+        ("2013-11-19T14:05:23.4-02:00", "2013-11-19T14:05:23.400000-02:00"),
+        ("2013-11-19T14:05:23.410+02:00", "2013-11-19T14:05:23.410000+02:00"),
+        ("2013-11-19T14:05:23.428-23:59", "2013-11-19T14:05:23.428000-23:59")))
+    def testConvertToString(self, input, output):
+        assert str(DateTime(input)) == output
+
+
+class TestTime:
+    """Tests for the suds.sax.date.Time class."""
+
+    def testConstructFromTime(self):
+        time = datetime.time(1, 1)
+        assert Time(time).value is time
+        time.replace(tzinfo=UtcTimezone())
+        assert Time(time).value is time
+
+    @pytest.mark.parametrize(("string", "h", "m", "s", "micros"), (
+        ("10:59:47", 10, 59, 47, 0),
+        ("9:9:13", 9, 9, 13, 0),
+        ("18:0:09.2139", 18, 0, 9, 213900),
+        ("18:0:09.02139", 18, 0, 9, 21390),
+        ("18:0:09.002139", 18, 0, 9, 2139),
+        ("0:00:00.00013", 0, 0, 0, 130),
+        ("0:00:00.000001", 0, 0, 0, 1),
+        ("0:00:00.000000", 0, 0, 0, 0),
+        ("23:59:6.999999", 23, 59, 6, 999999),
+        ("1:13:50.0", 1, 13, 50, 0)))
+    def testConstructFromString(self, string, h, m, s, micros):
+        assert Time(string).value == datetime.time(h, m, s, micros)
+
+    @pytest.mark.parametrize("string", _invalid_time_strings)
+    def testConstructFromString_failure(self, string):
+        pytest.raises(ValueError, Time, string)
+
+    @pytest.mark.parametrize(("string", "h", "m", "s", "micros"), (
+        ("0:0:0.0000000", 0, 0, 0, 0),
+        ("0:0:0.0000001", 0, 0, 0, 0),
+        ("0:0:0.0000004", 0, 0, 0, 0),
+        ("0:0:0.0000005", 0, 0, 0, 1),
+        ("0:0:0.0000006", 0, 0, 0, 1),
+        ("0:0:0.0000009", 0, 0, 0, 1),
+        ("0:0:0.5", 0, 0, 0, 500000),
+        ("0:0:0.5000004", 0, 0, 0, 500000),
+        ("0:0:0.5000005", 0, 0, 0, 500001),
+        ("0:0:0.50000050", 0, 0, 0, 500001),
+        ("0:0:0.50000051", 0, 0, 0, 500001),
+        ("0:0:0.50000055", 0, 0, 0, 500001),
+        ("0:0:0.50000059", 0, 0, 0, 500001),
+        ("0:0:0.5000006", 0, 0, 0, 500001),
+        ("0:0:0.9999990", 0, 0, 0, 999999),
+        ("0:0:0.9999991", 0, 0, 0, 999999),
+        ("0:0:0.9999994", 0, 0, 0, 999999),
+        ("0:0:0.99999949", 0, 0, 0, 999999),
+        ("0:0:0.9999995", 0, 0, 1, 0),
+        ("0:0:0.9999996", 0, 0, 1, 0),
+        ("0:0:0.9999999", 0, 0, 1, 0)))
+    def testConstructFromString_subsecondRounding(self, string, h, m, s,
+        micros):
+        assert Time(string).value == datetime.time(h, m, s, micros)
+
+    @pytest.mark.parametrize(
+        ("string", "h", "m", "s", "micros", "tz_h", "tz_m"), (
+        ("18:0:09.2139z", 18, 0, 9, 213900, 0, 0),
+        ("18:0:09.2139Z", 18, 0, 9, 213900, 0, 0),
+        ("18:0:09.2139+3", 18, 0, 9, 213900, 3, 0),
+        ("18:0:09.2139-3", 18, 0, 9, 213900, -3, 0),
+        ("18:0:09.2139-03", 18, 0, 9, 213900, -3, 0),
+        ("18:0:09.2139+9:3", 18, 0, 9, 213900, 9, 3),
+        ("18:0:09.2139+10:31", 18, 0, 9, 213900, 10, 31),
+        ("18:0:09.2139-10:31", 18, 0, 9, 213900, -10, -31)))
+    def testConstructFromString_timezone(self, string, h, m, s, micros, tz_h,
+        tz_m):
+        tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
+        tzinfo = FixedOffsetTimezone(tzdelta)
+        ref = datetime.time(h, m, s, micros, tzinfo=tzinfo)
+        assert Time(string).value == ref
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.date(2010, 10, 27),
+        datetime.datetime(2010, 10, 27, 10, 10)))
+    def testConstructFromUnknown(self, source):
+        pytest.raises(ValueError, Time, source)
+
+    @pytest.mark.parametrize(("input", "output"), (
+        ("14:05:23.428068", "14:05:23.428068"),
+        ("14:05:23.4280", "14:05:23.428000"),
+        ("23:59:59.9999995", "00:00:00"),
+        ("14:05:23.428068-3", "14:05:23.428068-03:00"),
+        ("14:05:23.068+03", "14:05:23.068000+03:00"),
+        ("14:05:23.4-02:00", "14:05:23.400000-02:00"),
+        ("14:05:23.410+02:00", "14:05:23.410000+02:00"),
+        ("14:05:23.428-23:59", "14:05:23.428000-23:59")))
+    def testConvertToString(self, input, output):
+        assert str(Time(input)) == output
+
+
+class TestXDate:
+    """
+    Tests for the suds.xsd.sxbuiltin.XDate class.
+
+    Python object <--> string conversion details already tested in TestDate.
+
+    """
+
+    def testTranslateEmptyStringToPythonObject(self):
+        assert XDate.translate("") == None
+
+    def testTranslateStringToPythonObject(self):
+        assert XDate.translate("1941-12-7") == datetime.date(1941, 12, 7)
+
+    def testTranslatePythonObjectToString(self):
+        date = datetime.date(2013, 7, 24)
+        translated = XDate.translate(date, topython=False)
+        assert isinstance(translated, str)
+        assert translated == "2013-07-24"
+
+    def testTranslatePythonObjectToString_datetime(self):
+        dt = datetime.datetime(2013, 7, 24, 11, 59, 4)
+        translated = XDate.translate(dt, topython=False)
+        assert isinstance(translated, str)
+        assert translated == "2013-07-24"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.time()))
+    def testTranslatePythonObjectToString_failed(self, source):
+        assert XDate.translate(source, topython=False) is source
+
+
+class TestXDateTime:
+    """
+    Tests for the suds.xsd.sxbuiltin.XDateTime class.
+
+    Python object <--> string conversion details already tested in
+    TestDateTime.
+
+    """
+
+    def testTranslateEmptyStringToPythonObject(self):
+        assert XDateTime.translate("") == None
+
+    def testTranslateStringToPythonObject(self):
+        dt = datetime.datetime(1941, 12, 7, 10, 30, 22, 454000)
+        assert XDateTime.translate("1941-12-7T10:30:22.454") == dt
+
+    def testTranslatePythonObjectToString(self):
+        dt = datetime.datetime(2021, 12, 31, 11, 25, tzinfo=UtcTimezone())
+        translated = XDateTime.translate(dt, topython=False)
+        assert isinstance(translated, str)
+        assert translated == "2021-12-31T11:25:00+00:00"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.time(22, 47, 9, 981),
+        datetime.date(2101, 1, 1)))
+    def testTranslatePythonObjectToString_failed(self, source):
+        assert XDateTime.translate(source, topython=False) is source
+
+
+class TestXTime:
+    """
+    Tests for the suds.xsd.sxbuiltin.XTime class.
+
+    Python object <--> string conversion details already tested in
+    TestDateTime.
+
+    """
+
+    def testTranslateEmptyStringToPythonObject(self):
+        assert XTime.translate("") == None
+
+    def testTranslateStringToPythonObject(self):
+        assert XTime.translate("10:30:22") == datetime.time(10, 30, 22)
+
+    def testTranslatePythonObjectToString(self):
+        time = datetime.time(16, 53, 12, tzinfo=FixedOffsetTimezone(4))
+        translated = XTime.translate(time, topython=False)
+        assert isinstance(translated, str)
+        assert translated == "16:53:12+04:00"
+
+    @pytest.mark.parametrize("source", (
+        None,
+        object(),
+        _Dummy(),
+        datetime.date(2101, 1, 1),
+        datetime.datetime(2101, 1, 1, 22, 47, 9, 981)))
+    def testTranslatePythonObjectToString_failed(self, source):
+        assert XTime.translate(source, topython=False) is source
diff --git a/tests/test_document_store.py b/tests/test_document_store.py
new file mode 100644
index 0000000..64fcf10
--- /dev/null
+++ b/tests/test_document_store.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library DocumentStore unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.store
+
+import pytest
+
+
+def test_accessing_DocumentStore_content():
+    content1 = suds.byte_str("one")
+    content2 = suds.byte_str("two")
+    content1_1 = suds.byte_str("one one")
+
+    store = suds.store.DocumentStore({"1":content1})
+    assert len(store) == 2
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+
+    store = suds.store.DocumentStore({"1":content1, "2":content2})
+    assert len(store) == 3
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+    __test_open(store, "2", content2)
+
+    store = suds.store.DocumentStore(uno=content1, due=content2)
+    assert len(store) == 3
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "uno", content1)
+    __test_open(store, "due", content2)
+
+    store = suds.store.DocumentStore({"1 1":content1_1})
+    assert len(store) == 2
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1 1", content1_1)
+
+    store = suds.store.DocumentStore({"1":content1, "1 1":content1_1})
+    assert len(store) == 3
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+    __test_open(store, "1 1", content1_1)
+
+
+def test_accessing_missing_DocumentStore_content():
+    store = suds.store.DocumentStore()
+    assert store.open("missing-content") is None
+    assert store.open("buga-wuga://missing-content") is None
+    assert store.open("ftp://missing-content") is None
+    assert store.open("http://missing-content") is None
+    assert store.open("https://missing-content") is None
+    pytest.raises(Exception, store.open, "suds://missing-content")
+
+
+def test_default_DocumentStore_instance():
+    assert len(suds.store.defaultDocumentStore) == 1
+    __test_default_DocumentStore_content(suds.store.defaultDocumentStore)
+
+
+def test_empty_DocumentStore_instance_is_not_shared():
+    assert suds.store.DocumentStore() is not suds.store.defaultDocumentStore
+    assert suds.store.DocumentStore() is not suds.store.DocumentStore()
+
+
+def test_updating_DocumentStore_content():
+    content1 = suds.byte_str("one")
+    content2 = suds.byte_str("two")
+    content1_1 = suds.byte_str("one one")
+
+    store = suds.store.DocumentStore()
+    assert len(store) == 1
+    __test_default_DocumentStore_content(store)
+
+    store.update({"1":content1})
+    assert len(store) == 2
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+
+    store.update({"1":content1, "2":content2, "1 1":content1_1})
+    assert len(store) == 4
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+    __test_open(store, "2", content2)
+    __test_open(store, "1 1", content1_1)
+
+    store.update({"2":content2, "1 1":content1_1})
+    assert len(store) == 4
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+    __test_open(store, "2", content2)
+    __test_open(store, "1 1", content1_1)
+
+    store.update(uno=content1, due=content2)
+    assert len(store) == 6
+    __test_default_DocumentStore_content(store)
+    __test_open(store, "1", content1)
+    __test_open(store, "2", content2)
+    __test_open(store, "1 1", content1_1)
+    __test_open(store, "uno", content1)
+    __test_open(store, "due", content2)
+
+
+def __test_default_DocumentStore_content(store):
+    __test_open(store, "schemas.xmlsoap.org/soap/encoding/",
+        suds.store.soap5_encoding_schema)
+
+
+def __test_open(store, location, expected_content):
+    assert store.open(location) is expected_content
+    assert store.open("buga-wuga://%s" % location) is expected_content
+    assert store.open("ftp://%s" % location) is expected_content
+    assert store.open("http://%s" % location) is expected_content
+    assert store.open("https://%s" % location) is expected_content
+    assert store.open("suds://%s" % location) is expected_content
diff --git a/tests/test_input_parameters.py b/tests/test_input_parameters.py
new file mode 100644
index 0000000..a4a6f01
--- /dev/null
+++ b/tests/test_input_parameters.py
@@ -0,0 +1,678 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library web service operation input parameter related unit tests.
+
+Suds provides the user with an option to automatically 'hide' wrapper elements
+around simple types and allow the user to specify such parameters without
+explicitly creating those wrappers. For example: operation taking a parameter
+of type X, where X is a sequence containing only a single simple data type
+(e.g. string or integer) will be callable by directly passing it that internal
+simple data type value instead of first wrapping that value in an object of
+type X and then passing that wrapper object instead.
+
+Unit tests in this module make sure suds recognizes an operation's input
+parameters in different scenarios as expected. It does not deal with binding
+given argument values to an operation's input parameters or constructing an
+actual binding specific web service operation invocation request, although they
+may use such functionality as tools indicating that suds recognized an
+operation's input parameters correctly.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import tests
+
+import pytest
+
+
+class Element:
+    """Represents elements in our XSD map test data."""
+
+    def __init__(self, name):
+        self.name = name
+
+
+class XSDType:
+    """Unwrapped parameter XSD type test data."""
+
+    def __init__(self, xsd, xsd_map):
+        self.xsd = xsd
+        self.xsd_map = xsd_map
+
+
+# Test data shared between different tests in this module.
+
+choice_choice = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:choice>
+          <xsd:element name="aString1" type="xsd:string" />
+          <xsd:element name="anInteger1" type="xsd:integer" />
+        </xsd:choice>
+        <xsd:choice>
+          <xsd:element name="aString2" type="xsd:string" />
+          <xsd:element name="anInteger2" type="xsd:integer" minOccurs="0" />
+        </xsd:choice>
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence", [
+            "choice_1", [
+                Element("aString1"),
+                Element("anInteger1")],
+            "choice_2", [
+                Element("aString2"),
+                Element("anInteger2")]]]])
+
+choice_element_choice = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:choice>
+          <xsd:element name="aString1" type="xsd:string" />
+          <xsd:element name="anInteger1" type="xsd:integer" />
+        </xsd:choice>
+        <xsd:element name="separator" type="xsd:string" />
+        <xsd:choice>
+          <xsd:element name="aString2" type="xsd:string" />
+          <xsd:element name="anInteger2" type="xsd:integer" minOccurs="0" />
+        </xsd:choice>
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence", [
+            "choice_1", [
+                Element("aString1"),
+                Element("anInteger1")],
+            Element("separator"),
+            "choice_2", [
+                Element("aString2"),
+                Element("anInteger2")]]]])
+
+choice_simple_nonoptional = XSDType("""\
+    <xsd:complexType>
+      <xsd:choice>
+        <xsd:element name="aString" type="xsd:string" />
+        <xsd:element name="anInteger" type="xsd:integer" />
+      </xsd:choice>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "choice", [
+            Element("aString"),
+            Element("anInteger")]]])
+
+choice_with_element_and_two_element_sequence = XSDType("""\
+    <xsd:complexType>
+      <xsd:choice>
+        <xsd:element name="a" type="xsd:integer" />
+        <xsd:sequence>
+          <xsd:element name="b1" type="xsd:integer" />
+          <xsd:element name="b2" type="xsd:integer" />
+        </xsd:sequence>
+      </xsd:choice>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "choice", [
+            Element("a"),
+            "sequence", [
+                Element("b1"),
+                Element("b2")]]]])
+
+empty_sequence = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence />
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence"]])
+
+sequence_choice_with_element_and_two_element_sequence = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:choice>
+          <xsd:element name="a" type="xsd:integer" />
+          <xsd:sequence>
+            <xsd:element name="b1" type="xsd:integer" />
+            <xsd:element name="b2" type="xsd:integer" />
+          </xsd:sequence>
+        </xsd:choice>
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence_1", [
+            "choice", [
+                Element("a"),
+                "sequence_2", [
+                    Element("b1"),
+                    Element("b2")]]]]])
+
+sequence_with_five_elements = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:element name="p1" type="xsd:string" />
+        <xsd:element name="p2" type="xsd:integer" />
+        <xsd:element name="p3" type="xsd:string" />
+        <xsd:element name="p4" type="xsd:integer" />
+        <xsd:element name="p5" type="xsd:string" />
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence", [
+            Element("p1"),
+            Element("p2"),
+            Element("p3"),
+            Element("p4"),
+            Element("p5")]]])
+
+sequence_with_one_element = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:element name="param" type="xsd:integer" />
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence", [
+            Element("param")]]])
+
+sequence_with_two_elements = XSDType("""\
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:element name="aString" type="xsd:string" />
+        <xsd:element name="anInteger" type="xsd:integer" />
+      </xsd:sequence>
+    </xsd:complexType>""", [
+    "complex_type", [
+        "sequence", [
+            Element("aString"),
+            Element("anInteger")]]])
+
+
+class TestUnsupportedParameterDefinitions:
+    """
+    Tests performed on WSDL schema's containing input parameter type
+    definitions that can not be modeled using the currently implemented suds
+    library input parameter definition structure.
+
+    The tests included in this group, most of which are expected to fail,
+    should serve as an illustration of what type of input parameter definitions
+    still need to be better modeled. Once this has been done, they should be
+    refactored into separate argument parsing, input parameter definition
+    structure and binding specific request construction tests.
+
+    """
+
+    def expect_error(self, expected_error_text, *args, **kwargs):
+        """
+        Assert a test function call raises an expected TypeError exception.
+
+        Caught exception is considered expected if its string representation
+        matches the given expected error text.
+
+        Expected error text may be given directly or as a list/tuple containing
+        valid alternatives.
+
+        Web service operation 'f' invoker is used as the default test function.
+        An alternate test function may be specified using the 'test_function'
+        keyword argument.
+
+        """
+        try:
+            test_function = kwargs.pop("test_function")
+        except KeyError:
+            test_function = self.service.f
+        e = pytest.raises(TypeError, test_function, *args, **kwargs).value
+        try:
+            if expected_error_text.__class__ in (list, tuple):
+                assert str(e) in expected_error_text
+            else:
+                assert str(e) == expected_error_text
+        finally:
+            del e
+
+    def init_function_params(self, params, **kwargs):
+        """
+        Initialize a test in this group with the given parameter definition.
+
+        Constructs a complete WSDL schema based on the given function parameter
+        definition (defines a single web service operation named 'f' by
+        default), and creates a suds Client object to be used for testing
+        suds's web service operation invocation.
+
+        An alternate operation name may be given using the 'operation_name'
+        keyword argument.
+
+        May only be invoked once per test.
+
+        """
+        input = '<xsd:element name="Wrapper">%s</xsd:element>' % (params,)
+        assert not hasattr(self, "service")
+        wsdl = tests.wsdl_input(input, "Wrapper", **kwargs)
+        client = tests.client_from_wsdl(wsdl, nosend=True)
+        self.service = client.service
+
+    @pytest.mark.parametrize("test_args_required", (
+        pytest.mark.xfail(reason="empty choice member items not supported")(
+            True),
+        False))
+    def test_choice_containing_an_empty_sequence(self, test_args_required):
+        """
+        Test reporting extra input parameters passed to a function taking a
+        choice parameter group containing an empty sequence subgroup.
+
+        """
+        self.init_function_params("""\
+          <xsd:complexType>
+            <xsd:choice>
+              <xsd:element name="a" type="xsd:integer" />
+              <xsd:sequence>
+              </xsd:sequence>
+            </xsd:choice>
+          </xsd:complexType>""")
+
+        expected = "f() takes 0 to 1 positional arguments but 3 were given"
+        if not test_args_required:
+            expected = [expected,
+                "f() takes 1 positional argument but 3 were given"]
+        self.expect_error(expected, 1, None, None)
+
+    @pytest.mark.parametrize("choice", (
+        # Explicitly marked as optional and containing only non-optional
+        # elements.
+        pytest.mark.xfail(reason="suds does not yet support minOccurs/"
+            "maxOccurs attributes on all/choice/sequence order indicators")(
+        """\
+          <xsd:complexType>
+            <xsd:choice minOccurs="0">
+              <xsd:element name="aString" type="xsd:string" />
+              <xsd:element name="anInteger" type="xsd:integer" />
+            </xsd:choice>
+          </xsd:complexType>"""),
+        # Explicitly marked as optional and containing at least one
+        # non-optional element.
+        """\
+          <xsd:complexType>
+            <xsd:choice minOccurs="0">
+              <xsd:element name="aString" type="xsd:string" minOccurs="0" />
+              <xsd:element name="anInteger" type="xsd:integer" />
+            </xsd:choice>
+          </xsd:complexType>""",
+        """\
+          <xsd:complexType>
+            <xsd:choice minOccurs="0">
+              <xsd:element name="aString" type="xsd:string" />
+              <xsd:element name="anInteger" type="xsd:integer" minOccurs="0" />
+            </xsd:choice>
+          </xsd:complexType>""",
+        """\
+          <xsd:complexType>
+            <xsd:choice minOccurs="0">
+              <xsd:element name="aString" type="xsd:string" minOccurs="0" />
+              <xsd:element name="anInteger" type="xsd:integer" minOccurs="0" />
+            </xsd:choice>
+          </xsd:complexType>"""))
+    def test_choice_explicitly_marked_as_optional(self, choice):
+        """
+        Test reporting extra input parameters passed to a function taking a
+        single optional choice parameter group.
+
+        """
+        self.init_function_params(choice)
+        expected = "f() takes 0 to 2 positional arguments but 3 were given"
+        self.expect_error(expected, "one", None, 3)
+
+
+ at pytest.mark.parametrize("part_name", ("uno", "due", "quatro"))
+def test_builtin_typed_element_parameter(part_name):
+    """
+    Test correctly recognizing web service operation input structure defined by
+    a built-in typed element.
+
+    """
+    wsdl = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="MyElement" type="xsd:integer" />
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="%s" element="ns:MyElement" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>""" % (part_name,))
+    client = tests.client_from_wsdl(wsdl, nosend=True)
+
+    # Collect references to required WSDL model content.
+    method = client.wsdl.services[0].ports[0].methods["f"]
+    assert not method.soap.input.body.wrapped
+    binding = method.binding.input
+    assert binding.__class__ is suds.bindings.document.Document
+    my_element = client.wsdl.schema.elements["MyElement", "my-namespace"]
+
+    param_defs = binding.param_defs(method)
+    _expect_params(param_defs, [("MyElement", my_element)])
+
+
+ at pytest.mark.parametrize("part_name", ("parameters", "pipi"))
+def test_explicitly_wrapped_parameter(part_name):
+    """
+    Test correctly recognizing explicitly wrapped web service operation input
+    structure which would otherwise be automatically unwrapped.
+
+    """
+    input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
+    wsdl = _unwrappable_wsdl(part_name, input_schema)
+    client = tests.client_from_wsdl(wsdl, nosend=True, unwrap=False)
+
+    # Collect references to required WSDL model content.
+    method = client.wsdl.services[0].ports[0].methods["f"]
+    assert not method.soap.input.body.wrapped
+    binding = method.binding.input
+    assert binding.__class__ is suds.bindings.document.Document
+    wrapper = client.wsdl.schema.elements["Wrapper", "my-namespace"]
+
+    param_defs = binding.param_defs(method)
+    _expect_params(param_defs, [("Wrapper", wrapper)])
+
+
+ at pytest.mark.parametrize("param_names", (
+    [],
+    ["parameters"],
+    ["pipi"],
+    ["fifi", "la", "fuff"]))
+def test_typed_parameters(param_names):
+    """
+    Test correctly recognizing web service operation input structure defined
+    with 0 or more typed input message part parameters.
+
+    """
+    wsdl = ["""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="MyType">
+        <xsd:sequence>
+          <xsd:element name="a" type="xsd:integer" />
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">"""]
+    for x in param_names:
+        part_def = '\n    <wsdl:part name="%s" type="ns:MyType" />' % (x,)
+        wsdl.append(part_def)
+    wsdl.append("""
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>""")
+    wsdl = suds.byte_str("".join(wsdl))
+    client = tests.client_from_wsdl(wsdl, nosend=True)
+
+    # Collect references to required WSDL model content.
+    method = client.wsdl.services[0].ports[0].methods["f"]
+    assert not method.soap.input.body.wrapped
+    binding = method.binding.input
+    assert binding.__class__ is suds.bindings.document.Document
+    my_type = client.wsdl.schema.types["MyType", "my-namespace"]
+
+    # Construct expected parameter definitions.
+    expected_param_defs = [
+        (param_name, [suds.bindings.binding.PartElement, param_name, my_type])
+        for param_name in param_names]
+
+    param_defs = binding.param_defs(method)
+    _expect_params(param_defs, expected_param_defs)
+
+
+ at pytest.mark.parametrize("xsd_type", (
+    choice_choice,
+    choice_element_choice,
+    choice_simple_nonoptional,
+    choice_with_element_and_two_element_sequence,
+    empty_sequence,
+    sequence_choice_with_element_and_two_element_sequence,
+    sequence_with_five_elements,
+    sequence_with_one_element,
+    sequence_with_two_elements))
+def test_unwrapped_parameter(xsd_type):
+    """Test recognizing unwrapped web service operation input structures."""
+    input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
+    wsdl = _unwrappable_wsdl("part_name", input_schema)
+    client = tests.client_from_wsdl(wsdl, nosend=True)
+
+    # Collect references to required WSDL model content.
+    method = client.wsdl.services[0].ports[0].methods["f"]
+    assert method.soap.input.body.wrapped
+    binding = method.binding.input
+    assert binding.__class__ is suds.bindings.document.Document
+    wrapper = client.wsdl.schema.elements["Wrapper", "my-namespace"]
+
+    # Construct expected parameter definitions.
+    xsd_map = sequence_choice_with_element_and_two_element_sequence.xsd_map
+    expected_param_defs = _parse_schema_model(wrapper, xsd_map)
+
+    param_defs = binding.param_defs(method)
+    _expect_params(param_defs, expected_param_defs)
+
+
+ at pytest.mark.parametrize("part_name", ("parameters", "pipi"))
+def test_unwrapped_parameter_part_name(part_name):
+    """
+    Unwrapped parameter's part name should not affect its parameter definition.
+
+    """
+    input_schema = sequence_choice_with_element_and_two_element_sequence.xsd
+    wsdl = _unwrappable_wsdl(part_name, input_schema)
+    client = tests.client_from_wsdl(wsdl, nosend=True)
+
+    # Collect references to required WSDL model content.
+    method = client.wsdl.services[0].ports[0].methods["f"]
+    assert method.soap.input.body.wrapped
+    binding = method.binding.input
+    assert binding.__class__ is suds.bindings.document.Document
+    wrapper = client.wsdl.schema.elements["Wrapper", "my-namespace"]
+
+    # Construct expected parameter definitions.
+    xsd_map = sequence_choice_with_element_and_two_element_sequence.xsd_map
+    expected_param_defs = _parse_schema_model(wrapper, xsd_map)
+
+    param_defs = binding.param_defs(method)
+    _expect_params(param_defs, expected_param_defs)
+
+
+def _expect_params(param_defs, expected_param_defs):
+    """
+    Assert the given parameter definition content.
+
+    Given expected parameter definition content may contain the expected
+    parameter type instance or it may contain a list/tuple describing the type
+    instead.
+
+    Type description list/tuple is expected to contain the following:
+      1. type object's class reference
+      2. type object's 'name' attribute value.
+      3. type object's resolved type instance reference
+
+    """
+    assert param_defs.__class__ is list
+    assert len(param_defs) == len(expected_param_defs)
+    for pdef, expected_pdef in zip(param_defs, expected_param_defs):
+        assert len(expected_pdef) in (2, 3), "bad test data"
+        assert pdef[0] == expected_pdef[0]  # name
+        if expected_pdef[1].__class__ in (list, tuple):
+            # type - class/name/type instance
+            assert pdef[1].__class__ is expected_pdef[1][0]
+            assert pdef[1].name == expected_pdef[1][1]
+            assert pdef[1].resolve() is expected_pdef[1][2]
+        else:
+            assert pdef[1] is expected_pdef[1]  # type - exact instance
+        assert pdef[2:] == expected_pdef[2:]  # ancestry - optional
+
+
+def _parse_schema_model(root, schema_model_map):
+    """
+    Utility function for preparing the expected parameter definition structure
+    based on an unwrapped input parameter's XSD type schema.
+
+    Parses the XSD schema definition under a given XSD schema item and returns
+    the expected parameter definition structure based on the given schema map.
+
+    The schema map describes the expected hierarchy of items in the given XSD
+    schema. Even though this information could be deduced from the XSD schema
+    itself, that would require a much more complex implementation and this is
+    supposed to be a simple testing utility.
+
+    """
+    schema_items = {}
+    param_defs = []
+    _parse_schema_model_r(schema_items, param_defs, [], root, schema_model_map)
+    return param_defs
+
+
+def _parse_schema_model_r(schema_items, param_defs, ancestry, parent,
+        schema_model_map):
+    """Recursive implementation detail for _parse_schema_model()."""
+    prev = None
+    ancestry = list(ancestry)
+    ancestry.append(parent)
+    n = 0
+    for x in schema_model_map:
+        if x.__class__ in (list, tuple):
+            assert prev is not None, "bad schema model map"
+            _parse_schema_model_r(schema_items, param_defs, ancestry, prev, x)
+            continue
+        item = parent.rawchildren[n]
+        if isinstance(x, Element):
+            x = x.name
+            prev = None
+            param_defs.append((x, item, ancestry))
+        else:
+            assert isinstance(x, str), "bad schema model map"
+            prev = item
+        assert x not in schema_items, "duplicate schema map item names"
+        schema_items[x] = item
+        n += 1
+    assert len(parent.rawchildren) == n
+
+
+def _unwrappable_wsdl(part_name, param_schema):
+    """
+    Return a WSDL schema byte string.
+
+    The returned WSDL schema defines a single service definition with a single
+    port containing a single function named 'f' taking automatically
+    unwrappable input parameter using document/literal binding.
+
+    The input parameter is defined as a single named input message part (name
+    given via the 'part_name' argument) referencing an XSD schema element named
+    'Wrapper' located in the 'my-namespace' namespace.
+
+    The wrapper element's type definition (XSD schema string) is given via the
+    'param_schema' argument.
+
+    """
+    return suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Wrapper">
+%(param_schema)s
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="%(part_name)s" element="ns:Wrapper" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="my-soap-action" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="unga-bunga-location" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>""" % {"param_schema":param_schema, "part_name":part_name})
diff --git a/tests/test_reply_handling.py b/tests/test_reply_handling.py
new file mode 100644
index 0000000..0a53724
--- /dev/null
+++ b/tests/test_reply_handling.py
@@ -0,0 +1,585 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Unit tests related to Suds Python library reply processing.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import tests
+
+import pytest
+
+import httplib
+import re
+import xml.sax
+
+
+def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_with_faults():
+    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
+        "status":s})
+    assert f("", None) is None
+    pytest.raises(Exception, f, "", httplib.INTERNAL_SERVER_ERROR)
+    assert f("", httplib.ACCEPTED) is None
+    assert f("", httplib.NO_CONTENT) is None
+    assert f("bla-bla", httplib.ACCEPTED) is None
+    assert f("bla-bla", httplib.NO_CONTENT) is None
+
+
+def test_ACCEPTED_and_NO_CONTENT_status_reported_as_None_without_faults():
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    f = lambda r, s : client.service.f(__inject={"reply":suds.byte_str(r),
+        "status":s})
+    assert f("", None) is not None
+    assert f("", httplib.INTERNAL_SERVER_ERROR) is not None
+    assert f("", httplib.ACCEPTED) is None
+    assert f("", httplib.NO_CONTENT) is None
+    assert f("bla-bla", httplib.ACCEPTED) is None
+    assert f("bla-bla", httplib.NO_CONTENT) is None
+
+
+def test_badly_formed_reply_XML():
+    for faults in (True, False):
+        client = tests.client_from_wsdl(_wsdl__simple, faults=faults)
+        pytest.raises(xml.sax.SAXParseException, client.service.f,
+            __inject={"reply":suds.byte_str("bad food")})
+
+
+# TODO: Update the current restriction type output parameter handling so such
+# parameters get converted to the correct Python data type based on the
+# restriction's underlying data type.
+ at pytest.mark.xfail
+def test_restriction_data_types():
+    client_unnamed = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="Elemento">
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:int">
+            <xsd:enumeration value="1" />
+            <xsd:enumeration value="3" />
+            <xsd:enumeration value="5" />
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>""", "Elemento"))
+
+    client_named = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:simpleType name="MyType">
+        <xsd:restriction base="xsd:int">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+
+    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:simpleType name="MyTypeGeneric">
+        <xsd:restriction base="xsd:int">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="2" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="4" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:simpleType name="MyType">
+        <xsd:restriction base="ns:MyTypeGeneric">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+
+    for client in (client_unnamed, client_named, client_twice_restricted):
+        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <Elemento xmlns="my-namespace">5</Elemento>
+  </env:Body>
+</env:Envelope>""")))
+        assert response.__class__ is int
+        assert response == 5
+
+
+def test_builtin_data_types(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    integer_type_mapping = {
+        "byte":int,
+        "int":int,
+        "integer":int,
+        "long":long,
+        "negativeInteger":int,
+        "nonNegativeInteger":int,
+        "nonPositiveInteger":int,
+        "positiveInteger":int,
+        "short":int,
+        "unsignedByte":int,
+        "unsignedInt":int,
+        "unsignedLong":long,
+        "unsignedShort":int}
+    for tag, type in integer_type_mapping.items():
+        client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="value" type="xsd:%s" />""" % tag, "value"))
+        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <value xmlns="my-namespace">15</value>
+  </env:Body>
+</env:Envelope>""")))
+        assert response.__class__ is type
+        assert response == 15
+
+    boolean_mapping = {"0":False, "1":True, "false":False, "true":True}
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="value" type="xsd:boolean" />""", "value"))
+    for value, expected_value in boolean_mapping.items():
+        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <value xmlns="my-namespace">%s</value>
+  </env:Body>
+</env:Envelope>""" % value)))
+        assert response.__class__ is bool
+        assert response == expected_value
+
+    # Suds implements no extra range checking.
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="value" type="xsd:byte" />""", "value"))
+    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <value xmlns="my-namespace">1500</value>
+  </env:Body>
+</env:Envelope>""")))
+    assert response.__class__ is int
+    assert response == 1500
+
+    #   Suds raises raw Python exceptions when it fails to convert received
+    # response element data to its mapped Python integer data type, according
+    # to the used WSDL schema.
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="value" type="xsd:int" />""", "value"))
+    e = pytest.raises(ValueError, client.service.f, __inject=dict(
+        reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <value xmlns="my-namespace">Fifteen</value>
+  </env:Body>
+</env:Envelope>"""))).value
+    # ValueError instance received here has different string representations
+    # depending on the Python version used:
+    #   Python 2.4:
+    #     "invalid literal for int(): Fifteen"
+    #   Python 2.7.3, 3.2.3:
+    #     "invalid literal for int() with base 10: 'Fifteen'"
+    assert re.match("invalid literal for int\(\)( with base 10)?: ('?)Fifteen"
+        "\\2$", str(e))
+
+    # Suds returns invalid boolean values as None.
+    invalid_boolean_values = ("True", "", "False", "2", "Fedora", "Z", "-1")
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="value" type="xsd:boolean" />""", "value"))
+    for value in invalid_boolean_values:
+        response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <value xmlns="my-namespace">%s</value>
+  </env:Body>
+</env:Envelope>""" % value)))
+        assert response is None
+
+
+def test_disabling_automated_simple_interface_unwrapping():
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="Elemento" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"), unwrap=False)
+    assert not _isOutputWrapped(client, "f")
+
+    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <Wrapper xmlns="my-namespace">
+        <Elemento>La-di-da-da-da</Elemento>
+    </Wrapper>
+  </env:Body>
+</env:Envelope>""")))
+
+    assert response.__class__.__name__ == "Wrapper"
+    assert len(response.__class__.__bases__) == 1
+    assert response.__class__.__bases__[0] is suds.sudsobject.Object
+    assert response.Elemento.__class__ is suds.sax.text.Text
+    assert response.Elemento == "La-di-da-da-da"
+
+
+def test_empty_reply():
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    f = lambda status=None, description=None : client.service.f(__inject=dict(
+        reply=suds.byte_str(), status=status, description=description))
+    status, reason = f()
+    assert status == httplib.OK
+    assert reason is None
+    status, reason = f(httplib.OK)
+    assert status == httplib.OK
+    assert reason is None
+    status, reason = f(httplib.INTERNAL_SERVER_ERROR)
+    assert status == httplib.INTERNAL_SERVER_ERROR
+    assert reason == 'injected reply'
+    status, reason = f(httplib.FORBIDDEN)
+    assert status == httplib.FORBIDDEN
+    assert reason == 'injected reply'
+    status, reason = f(httplib.FORBIDDEN, "kwack")
+    assert status == httplib.FORBIDDEN
+    assert reason == 'kwack'
+
+
+def test_fault_reply_with_unicode_faultstring(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    unicode_string = u"€ Jurko Gospodnetić ČĆŽŠĐčćžšđ"
+    fault_xml = suds.byte_str(u"""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <env:Fault>
+      <faultcode>env:Client</faultcode>
+      <faultstring>%s</faultstring>
+    </env:Fault>
+  </env:Body>
+</env:Envelope>
+""" % unicode_string)
+
+    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+    inject = dict(reply=fault_xml, status=httplib.INTERNAL_SERVER_ERROR)
+    e = pytest.raises(suds.WebFault, client.service.f, __inject=inject).value
+    assert e.fault.faultstring == unicode_string
+    assert e.document.__class__ is suds.sax.document.Document
+
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    status, fault = client.service.f(__inject=dict(reply=fault_xml,
+        status=httplib.INTERNAL_SERVER_ERROR))
+    assert status == httplib.INTERNAL_SERVER_ERROR
+    assert fault.faultstring == unicode_string
+
+
+def test_invalid_fault_namespace(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    fault_xml = suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:p="x">
+  <env:Body>
+    <p:Fault>
+      <faultcode>env:Client</faultcode>
+      <faultstring>Dummy error.</faultstring>
+      <detail>
+        <errorcode>ultimate</errorcode>
+      </detail>
+    </p:Fault>
+  </env:Body>
+</env:Envelope>
+""")
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+    inject = dict(reply=fault_xml, status=httplib.OK)
+    e = pytest.raises(Exception, client.service.f, __inject=inject).value
+    assert e.__class__ is Exception
+    assert str(e) == "<faultcode/> not mapped to message part"
+
+    for http_status in (httplib.INTERNAL_SERVER_ERROR,
+        httplib.PAYMENT_REQUIRED):
+        status, reason = client.service.f(__inject=dict(reply=fault_xml,
+            status=http_status, description="trla baba lan"))
+        assert status == http_status
+        assert reason == "trla baba lan"
+
+
+def test_missing_wrapper_response():
+    """
+    Suds library's automatic structure unwrapping should not be applied to
+    interpreting received SOAP Response XML.
+
+    """
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="fResponse" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+    assert _isOutputWrapped(client, "f")
+
+    response_with_missing_wrapper = client.service.f(__inject=dict(
+        reply=suds.byte_str("""<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <fResponse xmlns="my-namespace">Anything</fResponse>
+  </env:Body>
+</env:Envelope>""")))
+    assert response_with_missing_wrapper is None
+
+
+def test_reply_error_with_detail_with_fault(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+
+    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+        inject = dict(reply=_fault_reply__with_detail, status=http_status)
+        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
+        e = e.value
+        _test_fault(e.fault, True)
+        assert e.document.__class__ is suds.sax.document.Document
+        assert str(e) == "Server raised fault: 'Dummy error.'"
+
+    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
+        description="quack-quack")
+    e = pytest.raises(Exception, client.service.f, __inject=inject).value
+    assert e.__class__ is Exception
+    assert e.args[0][0] == httplib.BAD_REQUEST
+    assert e.args[0][1] == "quack-quack"
+
+
+def test_reply_error_with_detail_without_fault():
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+
+    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+        status, fault = client.service.f(__inject=dict(
+            reply=_fault_reply__with_detail, status=http_status))
+        assert status == httplib.INTERNAL_SERVER_ERROR
+        _test_fault(fault, True)
+
+    status, fault = client.service.f(__inject=dict(
+        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST))
+    assert status == httplib.BAD_REQUEST
+    assert fault == "injected reply"
+
+    status, fault = client.service.f(__inject=dict(
+        reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
+        description="haleluja"))
+    assert status == httplib.BAD_REQUEST
+    assert fault == "haleluja"
+
+
+def test_reply_error_without_detail_with_fault(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    client = tests.client_from_wsdl(_wsdl__simple, faults=True)
+
+    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+        inject = dict(reply=_fault_reply__without_detail, status=http_status)
+        e = pytest.raises(suds.WebFault, client.service.f, __inject=inject)
+        e = e.value
+        _test_fault(e.fault, False)
+        assert e.document.__class__ is suds.sax.document.Document
+        assert str(e) == "Server raised fault: 'Dummy error.'"
+
+    inject = dict(reply=_fault_reply__with_detail, status=httplib.BAD_REQUEST,
+        description="quack-quack")
+    e = pytest.raises(Exception, client.service.f, __inject=inject).value
+    assert e.__class__ is Exception
+    assert e.args[0][0] == httplib.BAD_REQUEST
+    assert e.args[0][1] == "quack-quack"
+
+
+def test_reply_error_without_detail_without_fault():
+    client = tests.client_from_wsdl(_wsdl__simple, faults=False)
+
+    for http_status in (httplib.OK, httplib.INTERNAL_SERVER_ERROR):
+        status, fault = client.service.f(__inject=dict(
+            reply=_fault_reply__without_detail, status=http_status))
+        assert status == httplib.INTERNAL_SERVER_ERROR
+        _test_fault(fault, False)
+
+    status, fault = client.service.f(__inject=dict(
+        reply=_fault_reply__without_detail, status=httplib.BAD_REQUEST,
+        description="kung-fu-fui"))
+    assert status == httplib.BAD_REQUEST
+    assert fault == "kung-fu-fui"
+
+
+def test_simple_bare_and_wrapped_output():
+    # Prepare web service proxies.
+    client_bare = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="fResponse" type="xsd:string" />""", "fResponse"))
+    client_wrapped = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="fResponse" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+
+    #   Make sure suds library inteprets our WSDL definitions as wrapped or
+    # bare output interfaces as expected.
+    assert not _isOutputWrapped(client_bare, "f")
+    assert _isOutputWrapped(client_wrapped, "f")
+
+    #   Both bare & wrapped single parameter output web service operation
+    # results get presented the same way even though the wrapped one actually
+    # has an extra wrapper element around its received output data.
+    data = "The meaning of life."
+    get_response = lambda client, x : client.service.f(__inject=dict(
+        reply=suds.byte_str(x)))
+
+    response_bare = get_response(client_bare, """<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <fResponse xmlns="my-namespace">%s</fResponse>
+  </env:Body>
+</env:Envelope>""" % data)
+    assert response_bare.__class__ is suds.sax.text.Text
+    assert response_bare == data
+
+    response_wrapped = get_response(client_wrapped, """<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <Wrapper xmlns="my-namespace">
+      <fResponse>%s</fResponse>
+    </Wrapper>
+  </env:Body>
+</env:Envelope>""" % data)
+    assert response_wrapped.__class__ is suds.sax.text.Text
+    assert response_wrapped == data
+
+
+def test_wrapped_sequence_output():
+    client = tests.client_from_wsdl(tests.wsdl_output("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="result1" type="xsd:string" />
+            <xsd:element name="result2" type="xsd:string" />
+            <xsd:element name="result3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+    assert _isOutputWrapped(client, "f")
+
+    response = client.service.f(__inject=dict(reply=suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <Wrapper xmlns="my-namespace">
+        <result1>Uno</result1>
+        <result2>Due</result2>
+        <result3>Tre</result3>
+    </Wrapper>
+  </env:Body>
+</env:Envelope>""")))
+
+    #   Composite replies always get unmarshalled as a dynamically constructed
+    # class named 'reply'.
+    assert len(response.__class__.__bases__) == 1
+    assert response.__class__.__name__ == "reply"
+    assert response.__class__.__bases__[0] is suds.sudsobject.Object
+
+    # Check response content.
+    assert len(response) == 3
+    assert response.result1 == "Uno"
+    assert response.result2 == "Due"
+    assert response.result3 == "Tre"
+    assert response.result1.__class__ is suds.sax.text.Text
+    assert response.result2.__class__ is suds.sax.text.Text
+    assert response.result3.__class__ is suds.sax.text.Text
+
+
+def _attibutes(object):
+    result = set()
+    for x in object:
+        result.add(x[0])
+    return result
+
+
+def _isOutputWrapped(client, method_name):
+    assert len(client.wsdl.bindings) == 1
+    operation = client.wsdl.bindings.values()[0].operations[method_name]
+    return operation.soap.output.body.wrapped
+
+
+def _test_fault(fault, has_detail):
+    assert fault.faultcode == "env:Client"
+    assert fault.faultstring == "Dummy error."
+    assert hasattr(fault, "detail") == has_detail
+    assert not has_detail or fault.detail.errorcode == "ultimate"
+    assert not hasattr(fault, "nonexisting")
+    expected_attributes = set(("faultcode", "faultstring"))
+    if has_detail:
+        expected_attributes.add("detail")
+    assert _attibutes(fault) == expected_attributes
+    assert not has_detail or _attibutes(fault.detail) == set(("errorcode",))
+
+
+_fault_reply__with_detail = suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <env:Fault>
+      <faultcode>env:Client</faultcode>
+      <faultstring>Dummy error.</faultstring>
+      <detail>
+        <errorcode>ultimate</errorcode>
+      </detail>
+    </env:Fault>
+  </env:Body>
+</env:Envelope>
+""")
+
+_fault_reply__without_detail = suds.byte_str("""\
+<?xml version="1.0"?>
+<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
+  <env:Body>
+    <env:Fault>
+      <faultcode>env:Client</faultcode>
+      <faultstring>Dummy error.</faultstring>
+    </env:Fault>
+  </env:Body>
+</env:Envelope>
+""")
+
+_wsdl__simple = tests.wsdl_output("""\
+      <xsd:element name="fResponse">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="output_i" type="xsd:integer" />
+            <xsd:element name="output_s" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "fResponse")
diff --git a/tests/test_request_construction.py b/tests/test_request_construction.py
new file mode 100644
index 0000000..94894f7
--- /dev/null
+++ b/tests/test_request_construction.py
@@ -0,0 +1,963 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds Python library request construction related unit tests.
+
+Suds provides the user with an option to automatically 'hide' wrapper elements
+around simple types and allow the user to specify such parameters without
+explicitly creating those wrappers. For example: function taking a parameter of
+type X, where X is a sequence containing only a single simple data type (e.g.
+string or integer) will be callable by directly passing it that internal simple
+data type value instead of first wrapping that value in an object of type X and
+then passing that wrapper object instead.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.store
+import tests
+
+import pytest
+
+
+# TODO: Update the current restriction type output parameter handling so such
+# parameters get converted to the correct Python data type based on the
+# restriction's underlying data type.
+ at pytest.mark.xfail
+def test_bare_input_restriction_types():
+    client_unnamed = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Elemento">
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string">
+            <xsd:enumeration value="alfa"/>
+            <xsd:enumeration value="beta"/>
+            <xsd:enumeration value="gamma"/>
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>""", "Elemento"))
+
+    client_named = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:simpleType name="MyType">
+        <xsd:restriction base="xsd:string">
+          <xsd:enumeration value="alfa"/>
+          <xsd:enumeration value="beta"/>
+          <xsd:enumeration value="gamma"/>
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:element name="Elemento" type="ns:MyType"/>""", "Elemento"))
+
+    assert not _isInputWrapped(client_unnamed, "f")
+    assert not _isInputWrapped(client_named, "f")
+
+
+def parametrize_single_element_input_test(param_names, param_values):
+    """
+    Define different parametrized single element input test function calls.
+
+    Parameter value input is a tuple containing 2+ parameters:
+      * 1. element - input XSD element definition
+      * 2. element - input element name
+      * 3+ elements - tuples containing the following:
+        * position argument list for the invoked test web service operation
+        * expected request body content for the given arguments
+        * [optional] reason for marking this test case as expected to fail
+
+    """
+    mark = pytest
+    expanded_param_values = []
+    for param_value in param_values:
+        xsd, external_element_name = param_value[0:2]
+        for next_value in param_value[2:]:
+            assert len(next_value) in (2, 3)
+            args, request_body = next_value[:2]
+            xfail = len(next_value) == 3
+            param = (xsd, external_element_name, args, request_body)
+            if xfail:
+                param = pytest.mark.xfail(param, reason=next_value[2])
+            expanded_param_values.append(param)
+    return (param_names, expanded_param_values), {}
+
+ at pytest.mark.indirect_parametrize(parametrize_single_element_input_test,
+    ("xsd", "external_element_name", "args", "request_body"), (
+    # Bare non-optional element.
+    ('<xsd:element name="a" type="xsd:integer"/>', "a",
+        ([], "<ns0:a/>"),
+        ([5], "<ns0:a>5</ns0:a>")),
+    # Bare optional element.
+    ('<xsd:element name="a" type="xsd:integer" minOccurs="0"/>', "a",
+        ([], ""),
+        ([5], "<ns0:a>5</ns0:a>")),
+    # Choice with a non-empty sub-sequence.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer"/>
+            <xsd:sequence>
+              <xsd:element name="b1" type="xsd:integer"/>
+              <xsd:element name="b2" type="xsd:integer"/>
+            </xsd:sequence>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 1], "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([None, 1, 2],
+            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2>2</ns0:b2></ns0:Wrapper>"),
+        ([None, None, 1],
+            "<ns0:Wrapper><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>",
+            "non-optional choice handling buggy")),
+    # Choice with a non-optional element.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+    # Choice with an optional element.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer" minOccurs="0"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+    # Choices with multiple elements, at least one of which is optional.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer" minOccurs="0"/>
+            <xsd:element name="b" type="xsd:integer"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer"/>
+            <xsd:element name="b" type="xsd:integer" minOccurs="0"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a" type="xsd:integer" minOccurs="0"/>
+            <xsd:element name="b" type="xsd:integer" minOccurs="0"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 5], "<ns0:Wrapper><ns0:b>5</ns0:b></ns0:Wrapper>")),
+    # Choice with multiple non-empty sub-sequences.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:sequence>
+              <xsd:element name="a1" type="xsd:integer"/>
+              <xsd:element name="a2" type="xsd:integer"/>
+            </xsd:sequence>
+            <xsd:sequence>
+              <xsd:element name="b1" type="xsd:integer"/>
+              <xsd:element name="b2" type="xsd:integer"/>
+            </xsd:sequence>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:a1/><ns0:a2/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([5], "<ns0:Wrapper><ns0:a1>5</ns0:a1><ns0:a2/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([5, 9], """\
+          <ns0:Wrapper>
+            <ns0:a1>5</ns0:a1>
+            <ns0:a2>9</ns0:a2>
+          </ns0:Wrapper>"""),
+        ([None, 1], "<ns0:Wrapper><ns0:a1/><ns0:a2>1</ns0:a2></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([None, None, 1],
+            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([None, None, 1, 2],
+            "<ns0:Wrapper><ns0:b1>1</ns0:b1><ns0:b2>2</ns0:b2></ns0:Wrapper>"),
+        ([None, None, None, 1],
+            "<ns0:Wrapper><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>",
+            "non-optional choice handling buggy")),
+    # Empty choice.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice/>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>")),
+    # Empty sequence.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence/>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>")),
+    # Optional choice.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:choice minOccurs="0">
+            <xsd:element name="a" type="xsd:integer"/>
+            <xsd:element name="b" type="xsd:integer"/>
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>",
+            # This test passes by accident - the following two bugs seem to
+            # cancel each other out:
+            #  - choice order indicators explicitly marked optional unsupported
+            #  - not constructing correct input parameter values when using no
+            #    input arguments for a choice
+            #"suds does not yet support minOccurs/maxOccurs attributes on "
+            #"all/choice/sequence order indicators"
+            ),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 1],
+            "<ns0:Wrapper><ns0:b>1</ns0:b></ns0:Wrapper>")),
+    # Optional sequence.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence minOccurs="0">
+            <xsd:element name="a" type="xsd:integer"/>
+            <xsd:element name="b" type="xsd:integer"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>",
+            "suds does not yet support minOccurs/maxOccurs attributes on all/"
+            "choice/sequence order indicators"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b/></ns0:Wrapper>"),
+        ([None, 1],
+            "<ns0:Wrapper><ns0:a/><ns0:b>1</ns0:b></ns0:Wrapper>"),
+        ([1, 2], """\
+            <ns0:Wrapper>
+              <ns0:a>1</ns0:a>
+              <ns0:b>2</ns0:b>
+            </ns0:Wrapper>""")),
+    # Sequence with a non-empty sub-sequence.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="a" type="xsd:integer"/>
+            <xsd:sequence>
+              <xsd:element name="b1" type="xsd:integer"/>
+              <xsd:element name="b2" type="xsd:integer"/>
+            </xsd:sequence>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:a/><ns0:b1/><ns0:b2/></ns0:Wrapper>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b1/><ns0:b2/></ns0:Wrapper>"),
+        ([None, 1],
+            "<ns0:Wrapper><ns0:a/><ns0:b1>1</ns0:b1><ns0:b2/></ns0:Wrapper>"),
+        ([None, 1, 2], """\
+            <ns0:Wrapper>
+              <ns0:a/>
+              <ns0:b1>1</ns0:b1>
+              <ns0:b2>2</ns0:b2>
+            </ns0:Wrapper>"""),
+        ([None, None, 1],
+            "<ns0:Wrapper><ns0:a/><ns0:b1/><ns0:b2>1</ns0:b2></ns0:Wrapper>")),
+    # Sequence with a non-optional element.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="a" type="xsd:integer"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:a/></ns0:Wrapper>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+    # Sequence with an optional element.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="a" type="xsd:integer" minOccurs="0"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>")),
+    # Sequence with multiple consecutive choices.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:choice>
+              <xsd:element name="aString1" type="xsd:string"/>
+              <xsd:element name="anInt1" type="xsd:integer"/>
+            </xsd:choice>
+            <xsd:choice>
+              <xsd:element name="aString2" type="xsd:string"/>
+              <xsd:element name="anInt2" type="xsd:integer" minOccurs="0"/>
+            </xsd:choice>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper><ns0:aString1/></ns0:Wrapper>",
+            "non-optional choice handling buggy"),
+        ([5], "<ns0:Wrapper><ns0:aString1>5</ns0:aString1></ns0:Wrapper>"),
+        ([None, 1, 2], """\
+            <ns0:Wrapper>
+              <ns0:anInt1>1</ns0:anInt1>
+              <ns0:aString2>2</ns0:aString2>
+            </ns0:Wrapper>"""),
+        ([None, 1, None, 2], """\
+            <ns0:Wrapper>
+              <ns0:anInt1>1</ns0:anInt1>
+              <ns0:anInt2>2</ns0:anInt2>
+            </ns0:Wrapper>""")),
+    # Sequence with multiple optional elements.
+    ("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="a" type="xsd:integer" minOccurs="0"/>
+            <xsd:element name="b" type="xsd:integer" minOccurs="0"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper",
+        ([], "<ns0:Wrapper/>"),
+        ([5], "<ns0:Wrapper><ns0:a>5</ns0:a></ns0:Wrapper>"),
+        ([None, 1], "<ns0:Wrapper><ns0:b>1</ns0:b></ns0:Wrapper>"),
+        ([5, 1],
+            "<ns0:Wrapper><ns0:a>5</ns0:a><ns0:b>1</ns0:b></ns0:Wrapper>")),
+    ))
+def test_document_literal_request_for_single_element_input(xsd,
+        external_element_name, args, request_body):
+    wsdl = tests.wsdl_input(xsd, external_element_name)
+    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+
+    assert _compare_request(client.service.f(*args), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>%s</ns1:Body>
+</SOAP-ENV:Envelope>""" % (request_body,))
+
+
+def test_disabling_automated_simple_interface_unwrapping():
+    client = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="Elemento" type="xsd:string"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"), nosend=True, prettyxml=True, unwrap=False)
+    assert not _isInputWrapped(client, "f")
+    wrapper = client.factory.create("Wrapper")
+    wrapper.Elemento = "Wonderwall"
+    assert _compare_request(client.service.f(Wrapper=wrapper), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:Elemento>Wonderwall</ns0:Elemento>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_element_references_to_different_namespaces():
+    wsdl = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns:tns="first-namespace"
+    targetNamespace="first-namespace">
+
+  <wsdl:types>
+    <xsd:schema
+        targetNamespace="first-namespace"
+        elementFormDefault="qualified"
+        attributeFormDefault="unqualified"
+        xmlns:second="second-namespace">
+      <xsd:import namespace="second-namespace" schemaLocation="suds://external_schema"/>
+      <xsd:element name="local_referenced" type="xsd:string"/>
+      <xsd:element name="fRequest">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="local" type="xsd:string"/>
+            <xsd:element ref="local_referenced"/>
+            <xsd:element ref="second:external"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="tns:fRequest"/>
+  </wsdl:message>
+
+  <wsdl:portType name="DummyServicePortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="tns:fRequestMessage"/>
+    </wsdl:operation>
+  </wsdl:portType>
+
+  <wsdl:binding name="DummyServiceBinding" type="tns:DummyServicePortType">
+    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f"/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+
+  <wsdl:service name="DummyService">
+    <wsdl:port name="DummyServicePort" binding="tns:DummyServiceBinding">
+      <soap:address location="BoogaWooga"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+""")
+
+    external_schema = suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<schema
+    xmlns="http://www.w3.org/2001/XMLSchema"
+    targetNamespace="second-namespace">
+  <element name="external" type="string"/>
+</schema>
+""")
+
+    store = suds.store.DocumentStore(external_schema=external_schema,
+        wsdl=wsdl)
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        nosend=True, prettyxml=True)
+    assert _compare_request(client.service.f(local="--L--",
+        local_referenced="--LR--", external="--E--"), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns1="first-namespace" xmlns:ns2="second-namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <SOAP-ENV:Body>
+      <ns1:fRequest>
+         <ns1:local>--L--</ns1:local>
+         <ns1:local_referenced>--LR--</ns1:local_referenced>
+         <ns2:external>--E--</ns2:external>
+      </ns1:fRequest>
+   </SOAP-ENV:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_invalid_input_parameter_type_handling():
+    """
+    Input parameters of invalid type get silently pushed into the constructed
+    SOAP request as strings, even though the constructed SOAP request does not
+    necessarily satisfy requirements set for it in the web service's WSDL
+    schema. It is then left up to the web service implementation to detect and
+    report this error.
+
+    """
+    client = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:complexType name="Freakazoid">
+        <xsd:sequence>
+          <xsd:element name="freak1" type="xsd:string"/>
+          <xsd:element name="freak2" type="xsd:string"/>
+          <xsd:element name="freak3" type="xsd:string"/>
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="p1" type="xsd:string"/>
+            <xsd:element name="anInteger" type="xsd:integer"/>
+            <xsd:element name="p2" type="xsd:string"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"), nosend=True, prettyxml=True)
+
+    # Passing an unrelated Python type value.
+    class SomeType:
+        def __str__(self):
+            return "Some string representation."
+    assert _compare_request(client.service.f(anInteger=SomeType()), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:p1/>
+         <ns0:anInteger>Some string representation.</ns0:anInteger>
+         <ns0:p2/>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    # Passing a value of a WSDL schema defined type.
+    value = client.factory.create("Freakazoid")
+    value.freak1 = "Tiny"
+    value.freak2 = "Miny"
+    value.freak3 = "Mo"
+    assert _compare_request(client.service.f(anInteger=value), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:p1/>
+         <ns0:anInteger>
+            <ns0:freak1>Tiny</ns0:freak1>
+            <ns0:freak2>Miny</ns0:freak2>
+            <ns0:freak3>Mo</ns0:freak3>
+         </ns0:anInteger>
+         <ns0:p2/>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_missing_parameters():
+    """Missing non-optional parameters should get passed as empty values."""
+    service = _service_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="aString" type="xsd:string"/>
+            <xsd:element name="anInteger" type="xsd:integer"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+
+    assert _compare_request(service.f(), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString/>
+         <ns0:anInteger/>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    assert _compare_request(service.f(u"Pero Ždero"), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString>Pero Ždero</ns0:aString>
+         <ns0:anInteger/>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    assert _compare_request(service.f(anInteger=666), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString/>
+         <ns0:anInteger>666</ns0:anInteger>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    # None value is treated the same as undefined.
+    assert _compare_request(service.f(aString=None, anInteger=666), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString/>
+         <ns0:anInteger>666</ns0:anInteger>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+    assert _compare_request(service.f(aString="Omega", anInteger=None), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString>Omega</ns0:aString>
+         <ns0:anInteger/>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_named_parameter():
+    class Tester:
+        def __init__(self, service, expected_xml):
+            self.service = service
+            self.expected_xml = expected_xml
+
+        def test(self, *args, **kwargs):
+            request = self.service.f(*args, **kwargs)
+            assert _compare_request(request, self.expected_xml)
+
+    # Test different ways to make the same web service operation call.
+    service = _service_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="uno" type="xsd:string"/>
+            <xsd:element name="due" type="xsd:string"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+    t = Tester(service, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:uno>einz</ns0:uno>
+         <ns0:due>zwei</ns0:due>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+    t.test("einz", "zwei")
+    t.test(uno="einz", due="zwei")
+    t.test(due="zwei", uno="einz")
+    t.test("einz", due="zwei")
+
+    #   The order of parameters in the constructed SOAP request should depend
+    # only on the initial WSDL schema.
+    service = _service_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="due" type="xsd:string"/>
+            <xsd:element name="uno" type="xsd:string"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+    t = Tester(service, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:due>zwei</ns0:due>
+         <ns0:uno>einz</ns0:uno>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+    t.test("zwei", "einz")
+    t.test(uno="einz", due="zwei")
+    t.test(due="zwei", uno="einz")
+    t.test("zwei", uno="einz")
+
+
+def test_optional_parameter_handling():
+    """Missing optional parameters should not get passed at all."""
+    service = _service_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="aString" type="xsd:string" minOccurs="0"/>
+            <xsd:element name="anInteger" type="xsd:integer" minOccurs="0"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper"))
+
+    assert _compare_request(service.f(), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper/>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    # None is treated as an undefined value.
+    assert _compare_request(service.f(None), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper/>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    # Empty string values are treated as well defined values.
+    assert _compare_request(service.f(""), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString></ns0:aString>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    assert _compare_request(service.f("Kiflica"), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString>Kiflica</ns0:aString>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    assert _compare_request(service.f(anInteger=666), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:anInteger>666</ns0:anInteger>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    assert _compare_request(service.f("Alfa", 9), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:aString>Alfa</ns0:aString>
+         <ns0:anInteger>9</ns0:anInteger>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_twice_wrapped_parameter():
+    """
+      Suds does not recognize 'twice wrapped' data structures and unwraps the
+    external one but keeps the internal wrapping structure in place.
+
+    """
+    client = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Wrapper1">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="Wrapper2">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="Elemento" type="xsd:string"/>
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper1"), nosend=True, prettyxml=True)
+
+    assert _isInputWrapped(client, "f")
+
+    # Web service operation calls made with 'valid' parameters.
+    #
+    # These calls are actually illegal and result in incorrectly generated SOAP
+    # requests not matching the relevant WSDL schema. To make them valid we
+    # would need to pass a more complex value instead of a simple string, but
+    # the current simpler solution is good enough for what we want to test
+    # here.
+    value = "A B C"
+    expectedRequest = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper1>
+         <ns0:Wrapper2>%s</ns0:Wrapper2>
+      </ns0:Wrapper1>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""" % (value,)
+    assert _compare_request(client.service.f(value), expectedRequest)
+    assert _compare_request(client.service.f(Wrapper2=value), expectedRequest)
+
+    # Web service operation calls made with 'invalid' parameters.
+    def testInvalidParameter(**kwargs):
+        assert len(kwargs) == 1
+        element = kwargs.keys()[0]
+        expected = "f() got an unexpected keyword argument '%s'" % (element,)
+        e = pytest.raises(TypeError, client.service.f, **kwargs).value
+        try:
+            assert str(e) == expected
+        finally:
+            del e
+    testInvalidParameter(Elemento="A B C")
+    testInvalidParameter(Wrapper1="A B C")
+
+
+def test_wrapped_parameter(monkeypatch):
+    monkeypatch.delitem(locals(), "e", False)
+
+    # Prepare web service proxies.
+    client = lambda *args : tests.client_from_wsdl(tests.wsdl_input(*args),
+        nosend=True, prettyxml=True)
+    client_bare_single = client("""\
+      <xsd:element name="Elemento" type="xsd:string"/>""", "Elemento")
+    client_bare_multiple_simple = client("""\
+      <xsd:element name="Elemento1" type="xsd:string"/>
+      <xsd:element name="Elemento2" type="xsd:string"/>""", "Elemento1",
+        "Elemento2")
+    client_bare_multiple_wrapped = client("""\
+      <xsd:complexType name="Wrapper">
+        <xsd:sequence>
+          <xsd:element name="Elemento" type="xsd:string"/>
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Elemento1" type="ns:Wrapper"/>
+      <xsd:element name="Elemento2" type="ns:Wrapper"/>""", "Elemento1",
+        "Elemento2")
+    client_wrapped_unnamed = client("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="Elemento" type="xsd:string"/>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper")
+    client_wrapped_named = client("""\
+      <xsd:complexType name="WrapperType">
+        <xsd:sequence>
+          <xsd:element name="Elemento" type="xsd:string"/>
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Wrapper" type="ns:WrapperType"/>""", "Wrapper")
+
+    #   Make sure suds library interprets our WSDL definitions as wrapped or
+    # bare input interfaces as expected.
+    assert not _isInputWrapped(client_bare_single, "f")
+    assert not _isInputWrapped(client_bare_multiple_simple, "f")
+    assert not _isInputWrapped(client_bare_multiple_wrapped, "f")
+    assert _isInputWrapped(client_wrapped_unnamed, "f")
+    assert _isInputWrapped(client_wrapped_named, "f")
+
+    #   Both bare & wrapped single parameter input web service operations get
+    # called the same way even though the wrapped one actually has an extra
+    # wrapper element around its input data.
+    data = "Maestro"
+    call_single = lambda c : c.service.f(data)
+
+    assert _compare_request(call_single(client_bare_single), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Elemento>%s</ns0:Elemento>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""" % data)
+
+    expected_xml = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Wrapper>
+         <ns0:Elemento>%s</ns0:Elemento>
+      </ns0:Wrapper>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""" % data
+    assert _compare_request(call_single(client_wrapped_unnamed), expected_xml)
+    assert _compare_request(call_single(client_wrapped_named), expected_xml)
+
+    #   Suds library's automatic structure unwrapping prevents us from
+    # specifying the external wrapper structure directly.
+    e = pytest.raises(TypeError, client_wrapped_unnamed.service.f, Wrapper="A")
+    assert str(e.value) == "f() got an unexpected keyword argument 'Wrapper'"
+
+    #   Multiple parameter web service operations are never automatically
+    # unwrapped.
+    data = ("Unga", "Bunga")
+    call_multiple = lambda c : c.service.f(*data)
+
+    assert _compare_request(call_multiple(client_bare_multiple_simple), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Elemento1>%s</ns0:Elemento1>
+      <ns0:Elemento2>%s</ns0:Elemento2>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""" % data)
+
+    assert _compare_request(call_multiple(client_bare_multiple_wrapped), """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Elemento1>%s</ns0:Elemento1>
+      <ns0:Elemento2>%s</ns0:Elemento2>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""" % data)
+
+
+def _compare_request(request, expected_xml):
+    return tests.compare_xml_to_string(request.original_envelope, expected_xml)
+
+
+def _isInputWrapped(client, method_name):
+    assert len(client.wsdl.bindings) == 1
+    operation = client.wsdl.bindings.values()[0].operations[method_name]
+    return operation.soap.input.body.wrapped
+
+
+def _service_from_wsdl(wsdl):
+    """
+    Construct a suds Client service instance used in tests in this module.
+
+    The constructed Client instance only prepares web service operation
+    invocation requests and does not attempt to actually send them.
+
+    """
+    client = tests.client_from_wsdl(wsdl, nosend=True, prettyxml=True)
+    return client.service
diff --git a/tests/test_suds.py b/tests/test_suds.py
new file mode 100644
index 0000000..614c976
--- /dev/null
+++ b/tests/test_suds.py
@@ -0,0 +1,2067 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the (LGPL) GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library Lesser General Public License for more details at
+# ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+General suds Python library unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+This whole module should be refactored into more specialized modules as more
+tests get added to it and it acquires more structure.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import tests
+
+import pytest
+
+import re
+import xml.sax
+
+
+# TODO: Update the current choice parameter handling implementation to make
+# this test pass.
+ at pytest.mark.xfail
+def test_choice_parameter_implementation_inconsistencies():
+    """
+    Choice parameter support implementation needs to be cleaned up.
+
+    If you declare a message part's element of a simple type X, or you define
+    it as a complex type having a single member of type X, and suds has been
+    configured to automatically unwrap such single-member complex types, the
+    web service proxy object's constructed function declarations should match.
+    They should both accept a single parameter of type X.
+
+    However the current choice support implementation causes only the 'complex'
+    case to get an additional 'choice' flag information to be included in the
+    constructed parameter definition structure.
+
+    """
+    client = lambda x, y : tests.client_from_wsdl(tests.wsdl_input(x, y))
+
+    client_simple_short = client("""\
+      <xsd:element name="Elemento" type="xsd:string" />""", "Elemento")
+
+    client_simple_long = client("""\
+      <xsd:element name="Elemento">
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:string" />
+        </xsd:simpleType>
+      </xsd:element>""", "Elemento")
+
+    client_complex_wrapped = client("""\
+      <xsd:element name="Wrapper">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="Elemento" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>""", "Wrapper")
+
+    method_param = lambda x : x.sd[0].ports[0][1][0][1][0]
+    method_param_simple_short = method_param(client_simple_short)
+    method_param_simple_long = method_param(client_simple_long)
+    method_param_complex_wrapped = method_param(client_complex_wrapped)
+
+    assert len(method_param_simple_short) == len(method_param_simple_long)
+    assert len(method_param_simple_long) == len(method_param_complex_wrapped)
+
+
+def test_converting_client_to_string_must_not_raise_an_exception():
+    client = tests.client_from_wsdl(suds.byte_str(
+        "<?xml version='1.0' encoding='UTF-8'?><root />"))
+    str(client)
+
+
+def test_converting_metadata_to_string():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="AAA">
+        <xsd:sequence>
+          <xsd:element name="u1" type="xsd:string" />
+          <xsd:element name="u2" type="xsd:string" />
+          <xsd:element name="u3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+    # Metadata with empty content.
+    metadata = client.wsdl.__metadata__
+    assert len(metadata) == 0
+    assert "<empty>" == str(metadata)
+
+    # Metadata with non-empty content.
+    metadata = client.factory.create("AAA").__metadata__
+    assert len(metadata) == 2
+    metadata_string = str(metadata)
+    assert re.search(" sxtype = ", metadata_string)
+    assert re.search(" ordering\[\] = ", metadata_string)
+
+
+def test_empty_invalid_wsdl(monkeypatch):
+    wsdl = suds.byte_str("")
+    monkeypatch.delitem(locals(), "e", False)
+    e = pytest.raises(xml.sax.SAXParseException, tests.client_from_wsdl, wsdl)
+    assert e.value.getMessage() == "no element found"
+
+
+def test_empty_valid_wsdl():
+    client = tests.client_from_wsdl(suds.byte_str(
+        "<?xml version='1.0' encoding='UTF-8'?><root />"))
+    assert not client.wsdl.services, "No service definitions must be read "  \
+        "from an empty WSDL."
+
+
+def test_enumeration_type_string_should_contain_its_value():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:simpleType name="AAA">
+        <xsd:restriction base="xsd:string">
+          <xsd:enumeration value="One" />
+          <xsd:enumeration value="Two" />
+          <xsd:enumeration value="Thirty-Two" />
+        </xsd:restriction>
+      </xsd:simpleType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+    enumeration_data = client.wsdl.schema.types["AAA", "my-namespace"]
+    # Legend:
+    #   eX - enumeration element.
+    #   aX - ancestry for the enumeration element.
+    (e1, a1), (e2, a2), (e3, a3) = enumeration_data
+    assert isinstance(e1, suds.xsd.sxbasic.Enumeration)
+    assert isinstance(e2, suds.xsd.sxbasic.Enumeration)
+    assert isinstance(e3, suds.xsd.sxbasic.Enumeration)
+    assert e1.name == "One"
+    assert e2.name == "Two"
+    assert e3.name == "Thirty-Two"
+    #   Python 3 output does not include a trailing L after long integer
+    # output, while Python 2 does. For example: 0x12345678 is output as
+    # 0x12345678L in Python 2 and simply as 0x12345678 in Python 3.
+    assert re.match('<Enumeration:0x[0-9a-f]+L? name="One" />$', e1.str())
+    assert re.match('<Enumeration:0x[0-9a-f]+L? name="Two" />$', e2.str())
+    assert re.match('<Enumeration:0x[0-9a-f]+L? name="Thirty-Two" />$',
+        e3.str())
+
+
+def test_function_parameters_global_sequence_in_a_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="UngaBunga">
+        <xsd:sequence>
+          <xsd:element name="u1" type="xsd:string" />
+          <xsd:element name="u2" type="xsd:string" />
+          <xsd:element name="u3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2" type="UngaBunga" />
+            <xsd:element name="x3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Elemento" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert len(service.types) == 1
+
+    # Method parameters as read from the service definition.
+    assert len(service.params) == 3
+    assert service.params[0][0].name == "x1"
+    assert service.params[0][0].type == _string_type
+    assert isinstance(service.params[0][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[1][0].name == "x2"
+    assert service.params[1][0].type == ("UngaBunga", "my-namespace")
+    assert isinstance(service.params[1][1], suds.xsd.sxbasic.Complex)
+    assert service.params[2][0].name == "x3"
+    assert service.params[2][0].type == _string_type
+    assert isinstance(service.params[2][1], suds.xsd.sxbuiltin.XString)
+
+    # Method parameters as read from a method object.
+    assert len(service.ports) == 1
+    port, methods = service.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+    assert len(method_params) == 3
+    assert method_params[0][0] == "x1"
+    assert method_params[0][1] is service.params[0][0]
+    assert method_params[1][0] == "x2"
+    assert method_params[1][1] is service.params[1][0]
+    assert method_params[2][0] == "x3"
+    assert method_params[2][1] is service.params[2][0]
+
+
+def test_function_parameters_local_choice():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="u1" type="xsd:string" />
+            <xsd:element name="u2" type="xsd:string" />
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Elemento" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert not service.types
+
+    # Method parameters as read from the service definition.
+    assert len(service.params) == 2
+    assert service.params[0][0].name == "u1"
+    assert service.params[0][0].type == _string_type
+    assert isinstance(service.params[0][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[1][0].name == "u2"
+    assert service.params[1][0].type == _string_type
+    assert isinstance(service.params[1][1], suds.xsd.sxbuiltin.XString)
+
+    # Method parameters as read from a method object.
+    assert len(service.ports) == 1
+    port, methods = service.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+    assert len(method_params) == 2
+    assert method_params[0][0] == "u1"
+    assert method_params[0][1] is service.params[0][0]
+    assert method_params[1][0] == "u2"
+    assert method_params[1][1] is service.params[1][0]
+
+    # Construct method parameter element object.
+    paramOut = client.factory.create("Elemento")
+    _assert_dynamic_type(paramOut, "Elemento")
+    assert not paramOut.__keylist__
+
+
+def test_function_parameters_local_choice_in_a_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2">
+              <xsd:complexType>
+                <xsd:choice>
+                  <xsd:element name="u1" type="xsd:string" />
+                  <xsd:element name="u2" type="xsd:string" />
+                  <xsd:element name="u3" type="xsd:string" />
+                </xsd:choice>
+              </xsd:complexType>
+            </xsd:element>
+            <xsd:element name="x3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Elemento" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert not service.types
+
+    # Method parameters as read from the service definition.
+    assert len(service.params) == 3
+    assert service.params[0][0].name == "x1"
+    assert service.params[0][0].type == _string_type
+    assert isinstance(service.params[0][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[1][0].name == "x2"
+    assert service.params[1][0].type is None
+    assert isinstance(service.params[1][1], suds.xsd.sxbasic.Element)
+    assert service.params[2][0].name == "x3"
+    assert service.params[2][0].type == _string_type
+    assert isinstance(service.params[2][1], suds.xsd.sxbuiltin.XString)
+
+    # Method parameters as read from a method object.
+    assert len(service.ports) == 1
+    port, methods = service.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+    assert len(method_params) == 3
+    assert method_params[0][0] == "x1"
+    assert method_params[0][1] is service.params[0][0]
+    assert method_params[1][0] == "x2"
+    assert method_params[1][1] is service.params[1][0]
+    assert method_params[2][0] == "x3"
+    assert method_params[2][1] is service.params[2][0]
+
+    # Construct method parameter element object.
+    paramOut = client.factory.create("Elemento")
+    _assert_dynamic_type(paramOut, "Elemento")
+    assert paramOut.x1 is None
+    _assert_dynamic_type(paramOut.x2, "x2")
+    assert not paramOut.x2.__keylist__
+    assert paramOut.x3 is None
+
+    # Construct method parameter objects with a locally defined type.
+    paramIn = client.factory.create("Elemento.x2")
+    _assert_dynamic_type(paramIn, "x2")
+    assert not paramOut.x2.__keylist__
+    assert paramIn is not paramOut.x2
+
+
+def test_function_parameters_local_sequence_in_a_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="u1" type="xsd:string" />
+                  <xsd:element name="u2" type="xsd:string" />
+                  <xsd:element name="u3" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+            <xsd:element name="x3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Elemento" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert not service.types
+
+    # Method parameters as read from the service definition.
+    assert len(service.params) == 3
+    assert service.params[0][0].name == "x1"
+    assert service.params[0][0].type == _string_type
+    assert isinstance(service.params[0][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[1][0].name == "x2"
+    assert service.params[1][0].type is None
+    assert isinstance(service.params[1][1], suds.xsd.sxbasic.Element)
+    assert service.params[2][0].name == "x3"
+    assert service.params[2][0].type == _string_type
+    assert isinstance(service.params[2][1], suds.xsd.sxbuiltin.XString)
+
+    # Method parameters as read from a method object.
+    assert len(service.ports) == 1
+    port, methods = service.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+    assert len(method_params) == 3
+    assert method_params[0][0] == "x1"
+    assert method_params[0][1] is service.params[0][0]
+    assert method_params[1][0] == "x2"
+    assert method_params[1][1] is service.params[1][0]
+    assert method_params[2][0] == "x3"
+    assert method_params[2][1] is service.params[2][0]
+
+    # Construct method parameter element object.
+    paramOut = client.factory.create("Elemento")
+    _assert_dynamic_type(paramOut, "Elemento")
+    assert paramOut.x1 is None
+    _assert_dynamic_type(paramOut.x2, "x2")
+    assert paramOut.x2.u1 is None
+    assert paramOut.x2.u2 is None
+    assert paramOut.x2.u3 is None
+    assert paramOut.x3 is None
+
+    # Construct method parameter objects with a locally defined type.
+    paramIn = client.factory.create("Elemento.x2")
+    _assert_dynamic_type(paramIn, "x2")
+    assert paramIn.u1 is None
+    assert paramIn.u2 is None
+    assert paramIn.u3 is None
+    assert paramIn is not paramOut.x2
+
+
+def test_function_parameters_sequence_in_a_choice():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Choice">
+        <xsd:complexType>
+          <xsd:choice>
+            <xsd:element name="a1" type="xsd:string" />
+            <xsd:element name="sequence">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="e1" type="xsd:string" />
+                  <xsd:element name="e2" type="xsd:string" />
+                  <xsd:element name="e3" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+            <xsd:element name="a2" type="xsd:string" />
+          </xsd:choice>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Choice" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    # Input #1.
+    request = _construct_SOAP_request(client, 'f', a1="Wackadoodle")
+    assert tests.compare_xml_to_string(request, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Choice>
+         <ns0:a1>Wackadoodle</ns0:a1>
+      </ns0:Choice>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+    # Input #2.
+    param = client.factory.create("Choice.sequence")
+    param.e2 = "Wackadoodle"
+    request = _construct_SOAP_request(client, 'f', sequence=param)
+    assert tests.compare_xml_to_string(request, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:Choice>
+         <ns0:sequence>
+            <ns0:e1/>
+            <ns0:e2>Wackadoodle</ns0:e2>
+            <ns0:e3/>
+         </ns0:sequence>
+      </ns0:Choice>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_function_parameters_sequence_in_a_choice_in_a_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="External">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="choice">
+              <xsd:complexType>
+                <xsd:choice>
+                  <xsd:element name="a1" type="xsd:string" />
+                  <xsd:element name="sequence">
+                    <xsd:complexType>
+                      <xsd:sequence>
+                        <xsd:element name="e1" type="xsd:string" />
+                        <xsd:element name="e2" type="xsd:string" />
+                        <xsd:element name="e3" type="xsd:string" />
+                      </xsd:sequence>
+                    </xsd:complexType>
+                  </xsd:element>
+                  <xsd:element name="a2" type="xsd:string" />
+                </xsd:choice>
+              </xsd:complexType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:External" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    # Construct input parameters.
+    param = client.factory.create("External.choice")
+    param.sequence = client.factory.create("External.choice.sequence")
+    param.sequence.e2 = "Wackadoodle"
+
+    # Construct a SOAP request containing our input parameters.
+    request = _construct_SOAP_request(client, 'f', param)
+    assert tests.compare_xml_to_string(request, """\
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:ns0="my-namespace" xmlns:ns1="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+   <SOAP-ENV:Header/>
+   <ns1:Body>
+      <ns0:External>
+         <ns0:choice>
+            <ns0:sequence>
+               <ns0:e1/>
+               <ns0:e2>Wackadoodle</ns0:e2>
+               <ns0:e3/>
+            </ns0:sequence>
+         </ns0:choice>
+      </ns0:External>
+   </ns1:Body>
+</SOAP-ENV:Envelope>""")
+
+
+def test_function_parameters_strings():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2" type="xsd:string" />
+            <xsd:element name="x3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:Elemento" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert not service.types
+
+    # Method parameters as read from the service definition.
+    assert len(service.params) == 3
+    assert service.params[0][0].name == "x1"
+    assert service.params[0][0].type == _string_type
+    assert isinstance(service.params[0][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[1][0].name == "x2"
+    assert service.params[1][0].type == _string_type
+    assert isinstance(service.params[1][1], suds.xsd.sxbuiltin.XString)
+    assert service.params[2][0].name == "x3"
+    assert service.params[2][0].type == _string_type
+    assert isinstance(service.params[2][1], suds.xsd.sxbuiltin.XString)
+
+    # Method parameters as read from a method object.
+    assert len(service.ports) == 1
+    port, methods = service.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+    assert len(method_params) == 3
+    assert method_params[0][0] == "x1"
+    assert method_params[0][1] is service.params[0][0]
+    assert method_params[1][0] == "x2"
+    assert method_params[1][1] is service.params[1][0]
+    assert method_params[2][0] == "x3"
+    assert method_params[2][1] is service.params[2][0]
+
+
+def test_global_enumeration():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:simpleType name="AAA">
+        <xsd:restriction base="xsd:string">
+          <xsd:enumeration value="One" />
+          <xsd:enumeration value="Two" />
+          <xsd:enumeration value="Thirty-Two" />
+        </xsd:restriction>
+      </xsd:simpleType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    assert len(client.sd) == 1
+    service = client.sd[0]
+
+    assert len(service.types) == 1
+    for typeTuple in service.types:
+        # Tuple containing the same object twice.
+        assert len(typeTuple) == 2
+        assert typeTuple[0] is typeTuple[1]
+
+    aType = service.types[0][0]
+    assert isinstance(aType, suds.xsd.sxbasic.Simple)
+    assert aType.name == "AAA"
+    assert aType.enum()
+    assert aType.mixed()
+    assert aType.restriction()
+    assert not aType.choice()
+    assert not aType.sequence()
+
+    assert len(aType.rawchildren) == 1
+    assert isinstance(aType.rawchildren[0], suds.xsd.sxbasic.Restriction)
+    assert aType.rawchildren[0].ref == _string_type
+
+    enum = client.factory.create("AAA")
+    assert enum.One == "One"
+    assert enum.Two == "Two"
+    assert getattr(enum, "Thirty-Two") == "Thirty-Two"
+
+
+def test_global_sequence_in_a_global_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Oklahoma">
+        <xsd:sequence>
+          <xsd:element name="c1" type="xsd:string" />
+          <xsd:element name="c2" type="xsd:string" />
+          <xsd:element name="c3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:complexType name="Wackadoodle">
+        <xsd:sequence>
+          <xsd:element name="x1" type="xsd:string" />
+          <xsd:element name="x2" type="Oklahoma" />
+          <xsd:element name="x3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+
+    assert len(service.types) == 2
+    for typeTuple in service.types:
+        # Tuple containing the same object twice.
+        assert len(typeTuple) == 2
+        assert typeTuple[0] is typeTuple[1]
+
+    aTypeIn = service.types[0][0]
+    assert isinstance(aTypeIn, suds.xsd.sxbasic.Complex)
+    assert aTypeIn.name == "Oklahoma"
+    assert not aTypeIn.sequence()
+    assert aTypeIn.rawchildren[0].sequence()
+
+    aTypeOut = service.types[1][0]
+    assert isinstance(aTypeOut, suds.xsd.sxbasic.Complex)
+    assert aTypeOut.name == "Wackadoodle"
+    assert not aTypeOut.sequence()
+    assert aTypeOut.rawchildren[0].sequence()
+
+    assert len(aTypeOut.rawchildren) == 1
+    children = aTypeOut.children()
+    assert isinstance(children, list)
+    assert len(children) == 3
+    assert children[0][0].name == "x1"
+    assert children[0][0].type == _string_type
+    assert children[1][0].name == "x2"
+    assert children[1][0].type == ("Oklahoma", "my-namespace")
+    assert children[2][0].name == "x3"
+    assert children[2][0].type == _string_type
+
+    sequenceOut = client.factory.create("Wackadoodle")
+    _assert_dynamic_type(sequenceOut, "Wackadoodle")
+    assert sequenceOut.__metadata__.sxtype is aTypeOut
+    assert sequenceOut.x1 is None
+    sequenceIn = sequenceOut.x2
+    assert sequenceOut.x3 is None
+    _assert_dynamic_type(sequenceIn, "Oklahoma")
+    assert sequenceIn.__metadata__.sxtype is aTypeIn
+    assert sequenceIn.c1 is None
+    assert sequenceIn.c2 is None
+    assert sequenceIn.c3 is None
+
+
+def test_global_string_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Oklahoma">
+        <xsd:sequence>
+          <xsd:element name="c1" type="xsd:string" />
+          <xsd:element name="c2" type="xsd:string" />
+          <xsd:element name="c3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+
+    assert len(service.types) == 1
+    for typeTuple in service.types:
+        # Tuple containing the same object twice.
+        assert len(typeTuple) == 2
+        assert typeTuple[0] is typeTuple[1]
+
+    aType = service.types[0][0]
+    assert isinstance(aType, suds.xsd.sxbasic.Complex)
+    assert aType.name == "Oklahoma"
+    assert not aType.choice()
+    assert not aType.enum()
+    assert not aType.mixed()
+    assert not aType.restriction()
+    assert not aType.sequence()
+
+    assert len(aType.rawchildren) == 1
+    sequence_node = aType.rawchildren[0]
+    assert isinstance(sequence_node, suds.xsd.sxbasic.Sequence)
+    assert sequence_node.sequence()
+    assert len(sequence_node) == 3
+    sequence_items = sequence_node.children()
+    assert isinstance(sequence_items, list)
+    assert len(sequence_items) == 3
+    assert sequence_items[0][0].name == "c1"
+    assert sequence_items[0][0].type == _string_type
+    assert sequence_items[1][0].name == "c2"
+    assert sequence_items[1][0].type == _string_type
+    assert sequence_items[2][0].name == "c3"
+    assert sequence_items[2][0].type == _string_type
+
+    sequence = client.factory.create("Oklahoma")
+    getattr(sequence, "c1")
+    getattr(sequence, "c2")
+    getattr(sequence, "c3")
+    pytest.raises(AttributeError, getattr, sequence, "nonExistingChild")
+    assert sequence.c1 is None
+    assert sequence.c2 is None
+    assert sequence.c3 is None
+    sequence.c1 = "Pero"
+    sequence.c3 = "Zdero"
+    assert sequence.c1 == "Pero"
+    assert sequence.c2 is None
+    assert sequence.c3 == "Zdero"
+
+
+def test_local_sequence_in_a_global_sequence():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Wackadoodle">
+        <xsd:sequence>
+          <xsd:element name="x1">
+              <xsd:complexType name="Oklahoma">
+                <xsd:sequence>
+                  <xsd:element name="c1" type="xsd:string" />
+                  <xsd:element name="c2" type="xsd:string" />
+                  <xsd:element name="c3" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="x2">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="s" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+          </xsd:element>
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    assert len(service.types) == 1
+
+    aTypeOut = service.types[0][0]
+    assert isinstance(aTypeOut, suds.xsd.sxbasic.Complex)
+    assert aTypeOut.name == "Wackadoodle"
+    assert not aTypeOut.sequence()
+    assert aTypeOut.rawchildren[0].sequence()
+
+    children = aTypeOut.children()
+    assert isinstance(children, list)
+    assert len(children) == 2
+    aTypeIn1 = children[0][0]
+    assert isinstance(aTypeIn1, suds.xsd.sxbasic.Element)
+    assert not aTypeIn1.sequence()
+    assert aTypeIn1.rawchildren[0].rawchildren[0].sequence()
+    aTypeIn2 = children[1][0]
+    assert isinstance(aTypeIn2, suds.xsd.sxbasic.Element)
+    assert not aTypeIn2.sequence()
+    assert aTypeIn2.rawchildren[0].rawchildren[0].sequence()
+    assert aTypeIn1.rawchildren[0].name == "Oklahoma"
+    assert aTypeIn1.rawchildren[0].type is None
+    namespace1 = aTypeIn1.rawchildren[0].namespace()
+    assert namespace1 == ("ns", "my-namespace")
+    assert aTypeIn2.rawchildren[0].name is None
+    assert aTypeIn2.rawchildren[0].type is None
+    assert aTypeIn1.rawchildren[0].namespace() is namespace1
+
+    sequenceOut = client.factory.create("Wackadoodle")
+    _assert_dynamic_type(sequenceOut, "Wackadoodle")
+    assert sequenceOut.__metadata__.sxtype is aTypeOut
+    sequenceIn1 = sequenceOut.x1
+    sequenceIn2 = sequenceOut.x2
+    _assert_dynamic_type(sequenceIn1, "x1")
+    _assert_dynamic_type(sequenceIn2, "x2")
+    assert sequenceIn1.__metadata__.sxtype is aTypeIn1
+    assert sequenceIn2.__metadata__.sxtype is aTypeIn2
+    assert sequenceIn1.c1 is None
+    assert sequenceIn1.c2 is None
+    assert sequenceIn1.c3 is None
+    assert sequenceIn2.s is None
+
+
+def test_no_trailing_comma_in_function_prototype_description_string__0():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="InputData">
+        <xsd:complexType>
+          <xsd:sequence />
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:InputData" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+    s = str(client)
+    assert " f()\n" in s
+
+
+def test_no_trailing_comma_in_function_prototype_description_string__1():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="InputData">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:InputData" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+    s = str(client)
+    assert " f(xs:string x1)\n" in s
+
+
+def test_no_trailing_comma_in_function_prototype_description_string__3():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="InputData">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2" type="xsd:string" />
+            <xsd:element name="x3" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:InputData" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+    s = str(client)
+    assert " f(xs:string x1, xs:string x2, xs:string x3)\n" in s
+
+
+def test_no_types():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema" />
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    assert len(client.sd) == 1
+    service = client.sd[0]
+
+    assert not client.wsdl.schema.types
+    assert not service.types
+
+    pytest.raises(suds.TypeNotFound, client.factory.create, "NonExistingType")
+
+
+def test_parameter_referencing_missing_element(monkeypatch):
+    wsdl = tests.wsdl_input("", "missingElement")
+    monkeypatch.delitem(locals(), "e", False)
+    e = pytest.raises(suds.TypeNotFound, tests.client_from_wsdl, wsdl).value
+    assert str(e) == "Type not found: '(missingElement, my-namespace, )'"
+
+
+# TODO: Update the current restriction type input parameter handling so they get
+# 'unwrapped' correctly instead of each of their enumeration values getting
+# interpreted as a separate input parameter.
+ at pytest.mark.xfail
+def test_restrictions():
+    client_unnamed = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:element name="Elemento">
+        <xsd:simpleType>
+          <xsd:restriction base="xsd:int">
+            <xsd:enumeration value="1" />
+            <xsd:enumeration value="3" />
+            <xsd:enumeration value="5" />
+          </xsd:restriction>
+        </xsd:simpleType>
+      </xsd:element>""", "Elemento"))
+
+    client_named = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:simpleType name="MyType">
+        <xsd:restriction base="xsd:int">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+
+    client_twice_restricted = tests.client_from_wsdl(tests.wsdl_input("""\
+      <xsd:simpleType name="MyTypeGeneric">
+        <xsd:restriction base="xsd:int">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="2" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="4" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:simpleType name="MyType">
+        <xsd:restriction base="ns:MyTypeGeneric">
+          <xsd:enumeration value="1" />
+          <xsd:enumeration value="3" />
+          <xsd:enumeration value="5" />
+        </xsd:restriction>
+      </xsd:simpleType>
+      <xsd:element name="Elemento" type="ns:MyType" />""", "Elemento"))
+
+    element_qref = ("Elemento", "my-namespace")
+    type_named_qref = ("MyType", "my-namespace")
+
+    element_unnamed = client_unnamed.wsdl.schema.elements[element_qref]
+    element_named = client_named.wsdl.schema.elements[element_qref]
+    element_twice_restricted = client_twice_restricted.wsdl.schema.elements[
+        element_qref]
+
+    type_unnamed = element_unnamed.resolve()
+    type_named = element_named.resolve()
+    type_twice_restricted = element_twice_restricted.resolve()
+    assert type_unnamed is element_unnamed
+    assert type_named is client_named.wsdl.schema.types[type_named_qref]
+    assert type_twice_restricted is client_twice_restricted.wsdl.schema.types[
+        type_named_qref]
+
+    #   Regression test against suds automatically unwrapping input parameter
+    # type's enumeration values as separate parameters.
+    params_unnamed = client_unnamed.sd[0].params
+    params_named = client_named.sd[0].params
+    params_twice_restricted = client_twice_restricted.sd[0].params
+    assert len(params_unnamed) == 1
+    assert len(params_named) == 1
+    assert len(params_twice_restricted) == 1
+    assert params_unnamed[0][0] is element_unnamed
+    assert params_unnamed[0][1] is type_unnamed
+    assert params_named[0][0] is element_named
+    assert params_named[0][1] is type_named
+    assert params_twice_restricted[0][0] is element_twice_restricted
+    assert params_twice_restricted[0][1] is type_twice_restricted
+
+
+def test_schema_node_occurrences():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+"""
+    + _element_node_xml("AnElement1")
+    + _element_node_xml("AnElement2", min=1)
+    + _element_node_xml("AnElement3", max=1)
+
+    + _element_node_xml("AnOptionalElement1", min=0)
+    + _element_node_xml("AnOptionalElement2", min=0, max=1)
+
+    + _element_node_xml("Array_0_2", min=0, max=2)
+    + _element_node_xml("Array_0_999", min=0, max=999)
+    + _element_node_xml("Array_0_X", min=0, max="unbounded")
+
+    + _element_node_xml("Array_x_2", max=2)
+    + _element_node_xml("Array_x_999", max=999)
+    + _element_node_xml("Array_x_X", max="unbounded")
+
+    + _element_node_xml("Array_1_2", min=1, max=2)
+    + _element_node_xml("Array_1_999", min=1, max=999)
+    + _element_node_xml("Array_1_X", min=1, max="unbounded")
+
+    + _element_node_xml("Array_5_5", min=5, max=5)
+    + _element_node_xml("Array_5_999", min=5, max=999)
+    + _element_node_xml("Array_5_X", min=5, max="unbounded")
++ """
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+    schema = client.wsdl.schema
+
+    def a(schema, name, min=None, max=None):
+        element = schema.elements[name, "my-namespace"]
+
+        if min is None:
+            assert element.min is None
+            min = 1
+        else:
+            assert str(min) == element.min
+        if max is None:
+            assert element.max is None
+            max = 1
+        else:
+            assert str(max) == element.max
+
+        expected_optional = min == 0
+        assert expected_optional == element.optional()
+
+        expected_required = not expected_optional
+        assert expected_required == element.required()
+
+        expected_multi_occurrence = (max == "unbounded") or (max > 1)
+        assert expected_multi_occurrence == element.multi_occurrence()
+
+    a(schema, "AnElement1")
+    a(schema, "AnElement2", min=1)
+    a(schema, "AnElement3", max=1)
+
+    a(schema, "AnOptionalElement1", min=0)
+    a(schema, "AnOptionalElement2", min=0, max=1)
+
+    a(schema, "Array_0_2", min=0, max=2)
+    a(schema, "Array_0_999", min=0, max=999)
+    a(schema, "Array_0_X", min=0, max="unbounded")
+
+    a(schema, "Array_x_2", max=2)
+    a(schema, "Array_x_999", max=999)
+    a(schema, "Array_x_X", max="unbounded")
+
+    a(schema, "Array_1_2", min=1, max=2)
+    a(schema, "Array_1_999", min=1, max=999)
+    a(schema, "Array_1_X", min=1, max="unbounded")
+
+    a(schema, "Array_5_5", min=5, max=5)
+    a(schema, "Array_5_999", min=5, max=999)
+    a(schema, "Array_5_X", min=5, max="unbounded")
+
+
+def test_schema_node_resolve():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Typo">
+        <xsd:sequence>
+          <xsd:element name="u1" type="xsd:string" />
+          <xsd:element name="u2" type="xsd:string" />
+          <xsd:element name="u3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2" type="Typo" />
+            <xsd:element name="x3">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="a1" type="xsd:string" />
+                  <xsd:element name="a2" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+      <xsd:element name="ElementoTyped" type="Typo" />
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+    schema = client.wsdl.schema
+
+    # Collect references to the test schema type nodes.
+    assert len(schema.types) == 1
+    typo = schema.types["Typo", "my-namespace"]
+    typo_u1 = typo.children()[0][0]
+    assert typo_u1.name == "u1"
+
+    # Collect references to the test schema element nodes.
+    assert len(schema.elements) == 2
+    elemento = schema.elements["Elemento", "my-namespace"]
+    elemento_x2 = elemento.children()[1][0]
+    assert elemento_x2.name == "x2"
+    elemento_x3 = elemento.children()[2][0]
+    assert elemento_x3.name == "x3"
+    elementoTyped = schema.elements["ElementoTyped", "my-namespace"]
+
+    # Resolving top-level locally defined non-content nodes.
+    assert typo.resolve() is typo
+
+    # Resolving a correctly typed top-level locally typed element.
+    assert elemento.resolve() is elemento
+
+    # Resolving top-level globally typed elements.
+    assert elementoTyped.resolve() is typo
+
+    # Resolving a subnode referencing a globally defined type.
+    assert elemento_x2.resolve() is typo
+
+    # Resolving a locally defined subnode.
+    assert elemento_x3.resolve() is elemento_x3
+
+    # Resolving builtin type nodes.
+    assert typo_u1.resolve().__class__ is suds.xsd.sxbuiltin.XString
+    assert typo_u1.resolve(nobuiltin=False).__class__ is  \
+        suds.xsd.sxbuiltin.XString
+    assert typo_u1.resolve(nobuiltin=True) is typo_u1
+    assert elemento_x2.resolve(nobuiltin=True) is typo
+    assert elemento_x3.resolve(nobuiltin=True) is elemento_x3
+
+
+def test_schema_node_resolve__nobuiltin_caching():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento1" type="xsd:string" />
+      <xsd:element name="Elemento2" type="xsd:string" />
+      <xsd:element name="Elemento3" type="xsd:string" />
+      <xsd:element name="Elemento4" type="xsd:string" />
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+    schema = client.wsdl.schema
+
+    # Collect references to the test schema element nodes.
+    assert len(schema.elements) == 4
+    e1 = schema.elements["Elemento1", "my-namespace"]
+    e2 = schema.elements["Elemento2", "my-namespace"]
+    e3 = schema.elements["Elemento3", "my-namespace"]
+    e4 = schema.elements["Elemento4", "my-namespace"]
+
+    #   Repeating the same resolve() call twice makes sure that the first call
+    # does not cache an incorrect value, thus causing the second call to return
+    # an incorrect result.
+
+    assert e1.resolve().__class__ is suds.xsd.sxbuiltin.XString
+    assert e1.resolve().__class__ is suds.xsd.sxbuiltin.XString
+
+    assert e2.resolve(nobuiltin=True) is e2
+    assert e2.resolve(nobuiltin=True) is e2
+
+    assert e3.resolve().__class__ is suds.xsd.sxbuiltin.XString
+    assert e3.resolve(nobuiltin=True) is e3
+    assert e3.resolve(nobuiltin=True) is e3
+
+    assert e4.resolve(nobuiltin=True) is e4
+    assert e4.resolve().__class__ is suds.xsd.sxbuiltin.XString
+    assert e4.resolve().__class__ is suds.xsd.sxbuiltin.XString
+
+
+def test_schema_node_resolve__invalid_type():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="Elemento1" type="Elemento1" />
+      <xsd:element name="Elemento2" type="Elemento1" />
+      <xsd:element name="Elemento3" type="XXX" />
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+    schema = client.wsdl.schema
+    assert len(schema.elements) == 3
+    elemento1 = schema.elements["Elemento1", "my-namespace"]
+    elemento2 = schema.elements["Elemento2", "my-namespace"]
+    elemento3 = schema.elements["Elemento3", "my-namespace"]
+    pytest.raises(suds.TypeNotFound, elemento1.resolve)
+    pytest.raises(suds.TypeNotFound, elemento2.resolve)
+    pytest.raises(suds.TypeNotFound, elemento3.resolve)
+
+
+def test_schema_node_resolve__references():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Typo">
+        <xsd:sequence>
+          <xsd:element name="u1" type="xsd:string" />
+          <xsd:element name="u2" type="xsd:string" />
+          <xsd:element name="u3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="ElementoTyped" type="Typo" />
+      <xsd:element name="ElementoTyped11" ref="ElementoTyped" />
+      <xsd:element name="ElementoTyped12" ref="ElementoTyped11" />
+      <xsd:element name="ElementoTyped13" ref="ElementoTyped12" />
+      <xsd:element name="ElementoTyped21" ref="ElementoTyped" />
+      <xsd:element name="ElementoTyped22" ref="ElementoTyped21" />
+      <xsd:element name="ElementoTyped23" ref="ElementoTyped22" />
+      <xsd:element name="ElementoTypedX" ref="ElementoTypedX" />
+      <xsd:element name="ElementoTypedX1" ref="ElementoTypedX2" />
+      <xsd:element name="ElementoTypedX2" ref="ElementoTypedX1" />
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+    schema = client.wsdl.schema
+
+    # Collect references to the test schema element & type nodes.
+    assert len(schema.types) == 1
+    typo = schema.types["Typo", "my-namespace"]
+    assert len(schema.elements) == 10
+    elementoTyped = schema.elements["ElementoTyped", "my-namespace"]
+    elementoTyped11 = schema.elements["ElementoTyped11", "my-namespace"]
+    elementoTyped12 = schema.elements["ElementoTyped12", "my-namespace"]
+    elementoTyped13 = schema.elements["ElementoTyped13", "my-namespace"]
+    elementoTyped21 = schema.elements["ElementoTyped21", "my-namespace"]
+    elementoTyped22 = schema.elements["ElementoTyped22", "my-namespace"]
+    elementoTyped23 = schema.elements["ElementoTyped23", "my-namespace"]
+    elementoTypedX = schema.elements["ElementoTypedX", "my-namespace"]
+    elementoTypedX1 = schema.elements["ElementoTypedX1", "my-namespace"]
+    elementoTypedX2 = schema.elements["ElementoTypedX2", "my-namespace"]
+
+    #   For referenced element node chains try resolving their nodes in both
+    # directions and try resolving them twice to try and avoid any internal
+    # resolve result caching that might cause some resursive resolution branch
+    # to not get taken.
+    #   Note that these assertions are actually redundant since inter-element
+    # references get processed and referenced type information merged back into
+    # the referencee when the schema information is loaded so no recursion is
+    # needed here in the first place. The tests should still be left in place
+    # and pass to serve as a safeguard in case this reference processing gets
+    # changed in the future.
+    assert elementoTyped11.resolve() is typo
+    assert elementoTyped11.resolve() is typo
+    assert elementoTyped13.resolve() is typo
+    assert elementoTyped13.resolve() is typo
+
+    assert elementoTyped23.resolve() is typo
+    assert elementoTyped23.resolve() is typo
+    assert elementoTyped21.resolve() is typo
+    assert elementoTyped21.resolve() is typo
+
+    # Recursive element references.
+    assert elementoTypedX.resolve() is elementoTypedX
+    assert elementoTypedX1.resolve() is elementoTypedX1
+    assert elementoTypedX2.resolve() is elementoTypedX2
+
+
+def test_schema_object_child_access_by_index():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="Oklahoma">
+        <xsd:sequence>
+          <xsd:element name="c1" type="xsd:string" />
+          <xsd:element name="c2" type="xsd:string" />
+          <xsd:element name="c3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:portType name="dummyPortType">
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    service = client.sd[0]
+    aType = service.types[0][0]
+    sequence = aType.rawchildren[0]
+    assert isinstance(sequence, suds.xsd.sxbasic.Sequence)
+    children = aType.children()
+    assert isinstance(children, list)
+
+    assert sequence[-1] is None
+
+    # TODO: Children are returned as a 2-tuple containing the child element and
+    # its ancestry (list of its parent elements). For some reason the ancestry
+    # list is returned as a new list on every __getitem__() call and so can not
+    # be compared using the 'is' operator. Also the children() function and
+    # accesing children by index does not seem to return ancestry lists of the
+    # same depth. See whether this can be updated so we always get the same
+    # ancestry list object.
+    # TODO: Add more detailed tests for the ancestry list structure.
+    # TODO: Add more detailed tests for the rawchildren list structure.
+
+    assert isinstance(sequence[0], tuple)
+    assert len(sequence[0]) == 2
+    assert sequence[0][0] is children[0][0]
+
+    assert isinstance(sequence[1], tuple)
+    assert len(sequence[1]) == 2
+    assert sequence[1][0] is children[1][0]
+
+    assert isinstance(sequence[2], tuple)
+    assert len(sequence[2]) == 2
+    assert sequence[2][0] is children[2][0]
+
+    assert sequence[3] is None
+
+
+def test_simple_wsdl():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:element name="f">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="a" type="xsd:string" />
+            <xsd:element name="b" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+      <xsd:element name="fResponse">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="c" type="xsd:string" />
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fRequestMessage">
+    <wsdl:part name="parameters" element="ns:f" />
+  </wsdl:message>
+  <wsdl:message name="fResponseMessage">
+    <wsdl:part name="parameters" element="ns:fResponse" />
+  </wsdl:message>
+  <wsdl:portType name="dummyPortType">
+    <wsdl:operation name="f">
+      <wsdl:input message="ns:fRequestMessage" />
+      <wsdl:output message="ns:fResponseMessage" />
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="dummy" type="ns:dummyPortType">
+    <soap:binding style="document"
+    transport="http://schemas.xmlsoap.org/soap/http" />
+    <wsdl:operation name="f">
+      <soap:operation soapAction="f" style="document" />
+      <wsdl:input><soap:body use="literal" /></wsdl:input>
+      <wsdl:output><soap:body use="literal" /></wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="dummy">
+    <wsdl:port name="dummy" binding="ns:dummy">
+      <soap:address location="https://localhost/dummy" />
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>
+"""))
+
+    # Target namespace.
+    assert client.wsdl.tns[0] == "ns"
+    assert client.wsdl.tns[1] == "my-namespace"
+
+    # Elements.
+    assert len(client.wsdl.schema.elements) == 2
+    elementIn = client.wsdl.schema.elements["f", "my-namespace"]
+    elementOut = client.wsdl.schema.elements["fResponse", "my-namespace"]
+    assert isinstance(elementIn, suds.xsd.sxbasic.Element)
+    assert isinstance(elementOut, suds.xsd.sxbasic.Element)
+    assert elementIn.name == "f"
+    assert elementOut.name == "fResponse"
+    assert len(elementIn.children()) == 2
+    param_in_1 = elementIn.children()[0][0]
+    param_in_2 = elementIn.children()[1][0]
+    assert param_in_1.name == "a"
+    assert param_in_1.type == _string_type
+    assert param_in_2.name == "b"
+    assert param_in_2.type == _string_type
+    assert len(elementOut.children()) == 1
+    param_out_1 = elementOut.children()[0][0]
+    assert param_out_1.name == "c"
+    assert param_out_1.type == _string_type
+
+    # Service definition.
+    assert len(client.sd) == 1
+    service_definition = client.sd[0]
+    assert service_definition.wsdl is client.wsdl
+
+    # Service.
+    assert len(client.wsdl.services) == 1
+    service = client.wsdl.services[0]
+    assert service_definition.service is service
+
+    # Ports.
+    assert len(service.ports) == 1
+    port = service.ports[0]
+    assert len(service_definition.ports) == 1
+    assert len(service_definition.ports[0]) == 2
+    assert service_definition.ports[0][0] is port
+
+    # Methods (from wsdl).
+    assert len(port.methods) == 1
+    method = port.methods["f"]
+    assert method.name == "f"
+    assert method.location == "https://localhost/dummy"
+
+    # Operations (from wsdl).
+    assert len(client.wsdl.bindings) == 1
+    binding_qname, binding = _first_from_dict(client.wsdl.bindings)
+    assert binding_qname == ("dummy", "my-namespace")
+    assert binding.__class__ is suds.wsdl.Binding
+    assert len(binding.operations) == 1
+    operation = binding.operations.values()[0]
+    input = operation.soap.input.body
+    output = operation.soap.output.body
+    assert len(input.parts) == 1
+    assert len(output.parts) == 1
+    input_element_qname = input.parts[0].element
+    output_element_qname = output.parts[0].element
+    assert input_element_qname == elementIn.qname
+    assert output_element_qname == elementOut.qname
+
+    # Methods (from service definition, for format specifications see the
+    # suds.serviceDefinition.ServiceDefinition.addports() docstring).
+    port, methods = service_definition.ports[0]
+    assert len(methods) == 1
+    method_name, method_params = methods[0]
+    assert method_name == "f"
+
+    param_name, param_element, param_ancestry = method_params[0]
+    assert param_name == "a"
+    assert param_element is param_in_1
+    assert len(param_ancestry) == 3
+    assert type(param_ancestry[0]) is suds.xsd.sxbasic.Element
+    assert param_ancestry[0].name == "f"
+    assert type(param_ancestry[1]) is suds.xsd.sxbasic.Complex
+    assert type(param_ancestry[2]) is suds.xsd.sxbasic.Sequence
+
+    param_name, param_element, param_ancestry = method_params[1]
+    assert param_name == "b"
+    assert param_element is param_in_2
+    assert len(param_ancestry) == 3
+    assert type(param_ancestry[0]) is suds.xsd.sxbasic.Element
+    assert param_ancestry[0].name == "f"
+    assert type(param_ancestry[1]) is suds.xsd.sxbasic.Complex
+    assert type(param_ancestry[2]) is suds.xsd.sxbasic.Sequence
+
+
+def test_wsdl_schema_content():
+    client = tests.client_from_wsdl(suds.byte_str("""\
+<?xml version='1.0' encoding='UTF-8'?>
+<wsdl:definitions targetNamespace="my-namespace"
+xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+xmlns:ns="my-namespace"
+xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
+  <wsdl:types>
+    <xsd:schema targetNamespace="my-namespace"
+    elementFormDefault="qualified"
+    attributeFormDefault="unqualified"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+      <xsd:complexType name="UngaBunga">
+        <xsd:sequence>
+          <xsd:element name="u1" type="xsd:string" />
+          <xsd:element name="u2" type="xsd:string" />
+          <xsd:element name="u3" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:complexType name="Fifi">
+        <xsd:sequence>
+          <xsd:element name="x" type="xsd:string" />
+        </xsd:sequence>
+      </xsd:complexType>
+      <xsd:element name="Elemento">
+        <xsd:complexType>
+          <xsd:sequence>
+            <xsd:element name="x1" type="xsd:string" />
+            <xsd:element name="x2" type="UngaBunga" />
+            <xsd:element name="x3">
+              <xsd:complexType>
+                <xsd:sequence>
+                  <xsd:element name="a1" type="xsd:string" />
+                  <xsd:element name="a2" type="xsd:string" />
+                </xsd:sequence>
+              </xsd:complexType>
+            </xsd:element>
+          </xsd:sequence>
+        </xsd:complexType>
+      </xsd:element>
+    </xsd:schema>
+  </wsdl:types>
+</wsdl:definitions>
+"""))
+
+    # Elements.
+    assert len(client.wsdl.schema.elements) == 1
+    elemento = client.wsdl.schema.elements["Elemento", "my-namespace"]
+    assert isinstance(elemento, suds.xsd.sxbasic.Element)
+
+    pytest.raises(KeyError, client.wsdl.schema.elements.__getitem__,
+        ("DoesNotExist", "OMG"))
+
+    # Types.
+    assert len(client.wsdl.schema.types) == 2
+    unga_bunga = client.wsdl.schema.types["UngaBunga", "my-namespace"]
+    assert isinstance(unga_bunga, suds.xsd.sxbasic.Complex)
+    fifi = client.wsdl.schema.types["Fifi", "my-namespace"]
+    assert isinstance(unga_bunga, suds.xsd.sxbasic.Complex)
+
+    pytest.raises(KeyError, client.wsdl.schema.types.__getitem__,
+        ("DoesNotExist", "OMG"))
+
+
+def _assert_dynamic_type(anObject, typename):
+    assert anObject.__module__ == suds.sudsobject.__name__
+    assert anObject.__metadata__.sxtype.name == typename
+    #   In order to be compatible with old style classes (py2 only) we need to
+    # access the object's class information using its __class__ member and not
+    # the type() function. type() function always returns <type 'instance'> for
+    # old-style class instances while the __class__ member returns the correct
+    # class information for both old and new-style classes.
+    assert anObject.__class__.__module__ == suds.sudsobject.__name__
+    assert anObject.__class__.__name__ == typename
+
+
+def _construct_SOAP_request(client, operation_name, *args, **kwargs):
+    """
+    Returns a SOAP request for a given web service operation invocation.
+
+      To make the test case code calling this function simpler, assumes we want
+    to call the operation on the given client's first service & port.
+
+    """
+    method = client.wsdl.services[0].ports[0].methods[operation_name]
+    return method.binding.input.get_message(method, args, kwargs)
+
+
+def _element_node_xml(name, min=None, max=None):
+    s = []
+    s.append('      <xsd:element name="')
+    s.append(name)
+    s.append('" type="xsd:string" ')
+    if min is not None:
+        s.append('minOccurs="%s" ' % (min,))
+    if max is not None:
+        s.append('maxOccurs="%s" ' % (max,))
+    s.append('/>\n')
+    return ''.join(s)
+
+
+def _first_from_dict(d):
+    """Returns the first name/value pair from a dictionary or None if empty."""
+    for x in d.items():
+        return x[0], x[1]
+
+
+_string_type = ("string", "http://www.w3.org/2001/XMLSchema")
diff --git a/tests/test_timezone.py b/tests/test_timezone.py
new file mode 100644
index 0000000..c25db01
--- /dev/null
+++ b/tests/test_timezone.py
@@ -0,0 +1,79 @@
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Unit tests for Timezone modeling classes implemented in suds.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+from suds.sax.date import FixedOffsetTimezone, UtcTimezone
+import tests
+
+import pytest
+
+import datetime
+
+
+class TestFixedOffsetTimezone:
+    """Tests for the suds.sax.date.FixedOffsetTimezone class."""
+
+    @pytest.mark.parametrize(("h", "m", "name"), (
+        (-13, 0, "-13:00"),
+        (-5, 0, "-05:00"),
+        (0, 0, "+00:00"),
+        (5, 0, "+05:00"),
+        (13, 0, "+13:00"),
+        (5, 50, "+05:50"),
+        (-4, 31, "-04:31")))
+    def test(self, h, m, name):
+        tz_delta = datetime.timedelta(hours=h, minutes=m)
+        tz = FixedOffsetTimezone(tz_delta)
+        assert tz.utcoffset(None) is tz_delta
+        assert tz.dst(None) == datetime.timedelta(0)
+        assert tz.tzname(None) == name
+        assert str(tz) == "FixedOffsetTimezone " + name
+
+    @pytest.mark.parametrize(("h", "m", "s", "us"), (
+        (-22, 10, 1, 0),
+        (-5, 0, 59, 0),
+        (0, 0, 0, 1),
+        (12, 12, 0, 120120),
+        (12, 12, 0, 999999)))
+    def testTooPreciseOffset(self, h, m, s, us):
+        o = datetime.timedelta(hours=h, minutes=m, seconds=s, microseconds=us)
+        pytest.raises(ValueError, FixedOffsetTimezone, o)
+
+    @pytest.mark.parametrize("hours", (-5, 0, 5))
+    def testConstructFromInteger(self, hours):
+        tz = FixedOffsetTimezone(hours)
+        assert tz.utcoffset(None) == datetime.timedelta(hours=hours)
+
+
+class TestUtcTimezone:
+    """Tests for the suds.sax.date.UtcTimezone class."""
+
+    def test(self):
+        tz = UtcTimezone()
+        assert tz.utcoffset(None) == datetime.timedelta(0)
+        assert tz.dst(None) == datetime.timedelta(0)
+        assert tz.tzname(None) == "UTC"
+        assert str(tz) == "UtcTimezone"
diff --git a/tests/test_transport.py b/tests/test_transport.py
new file mode 100644
index 0000000..8cd0454
--- /dev/null
+++ b/tests/test_transport.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds library transport related unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+from suds.transport import Reply, Request
+
+import pytest
+
+import sys
+
+
+ at pytest.mark.parametrize("message", (
+    u"",
+    u"for a bitch it's haaaard...",
+    u"I'm here to kick ass,\nand chew bubble gum...\nand I'm all out of gum.",
+    u"šuć-muć pa ožeži.. za 100 €\n\nwith multiple\nlines...",
+    u"\n\n\n\n\n\n",
+    u"中原千军逐蒋"))
+def test_reply_as_string(message):
+    code = 17
+    reply = Reply(code, {"aaa":1}, message)
+    expected = u"""\
+CODE: %s
+HEADERS: %s
+MESSAGE:
+%s""" % (code, reply.headers, message)
+    assert unicode(reply) == expected
+    if sys.version_info < (3, 0):
+        assert str(reply) == expected.encode("utf-8")
+
+
+ at pytest.mark.parametrize(("code", "headers", "message"), (
+    (1, {}, "ola"),
+    (2, {"semper":"fi"}, u"中原千军逐蒋\n城楼万众检阅")))
+def test_reply_constructor(code, headers, message):
+    reply = Reply(code, headers, message)
+    assert reply.code == code
+    assert reply.headers == headers
+    assert reply.message == message
+
+
+ at pytest.mark.parametrize("message", (
+    u"",
+    u"for a bitch it's haaaard...",
+    u"I'm here to kick ass,\nand chew bubble gum...\nand I'm all out of gum.",
+    u"šuć-muć pa ožeži.. za 100 €\n\nwith multiple\nlines...",
+    u"\n\n\n\n\n\n",
+    u"中原千军逐蒋"))
+def test_request_as_string(message):
+    request = Request("my url", message)
+    request.headers["aaa"] = 1
+    expected = u"""\
+URL: my url
+HEADERS: %s
+MESSAGE:
+%s""" % (request.headers, message)
+    assert unicode(request) == expected
+    if sys.version_info < (3, 0):
+        assert str(request) == expected.encode("utf-8")
+
+
+ at pytest.mark.parametrize(("url", "message"), (
+    ("for a bitch it's haaaard...", "it's hard out here..."),
+    (u"中原千军逐蒋", u"城楼万众检阅")))
+def test_request_constructor(url, message):
+    request = Request(url, message)
+    assert request.url == url
+    assert request.message == message
+    assert request.headers == {}
+
+
+def test_request_without_message():
+    request = Request("for a bitch it's haaaard...")
+    assert request.url == "for a bitch it's haaaard..."
+    assert request.message is None
+    assert request.headers == {}
diff --git a/tests/test_transport_http.py b/tests/test_transport_http.py
new file mode 100644
index 0000000..1502b0d
--- /dev/null
+++ b/tests/test_transport_http.py
@@ -0,0 +1,398 @@
+# -*- coding: utf-8 -*-
+
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of the (LGPL) GNU Lesser General Public License as published by the
+# Free Software Foundation; either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
+# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# written by: Jurko Gospodnetić ( jurko.gospodnetic at pke.hr )
+
+"""
+Suds library HTTP transport related unit tests.
+
+Implemented using the 'pytest' testing framework.
+
+"""
+
+if __name__ == "__main__":
+    import __init__
+    __init__.runUsingPyTest(globals())
+
+
+import suds
+import suds.client
+import suds.store
+import suds.transport.http
+
+import pytest
+
+import base64
+import sys
+import urllib2
+
+
+class MyException(Exception):
+    """Local exception used in this test module."""
+    pass
+
+
+def test_authenticated_http():
+    t = suds.transport.http.HttpAuthenticated(username="Habul AfuFa",
+        password="preCious")
+    assert t.credentials() == ("Habul AfuFa", "preCious")
+
+    t = suds.transport.http.HttpAuthenticated(username="macro")
+    assert t.credentials() == ("macro", None)
+
+
+def test_authenticated_http_add_credentials_to_request():
+    class MockRequest:
+        def __init__(self):
+            self.headers = {}
+
+    t = suds.transport.http.HttpAuthenticated(username="Humpty")
+    r = MockRequest()
+    t.addcredentials(r)
+    assert len(r.headers) == 0
+
+    t = suds.transport.http.HttpAuthenticated(password="Dumpty")
+    r = MockRequest()
+    t.addcredentials(r)
+    assert len(r.headers) == 0
+
+    username = "Habul Afufa"
+    password = "preCious"
+    t = suds.transport.http.HttpAuthenticated(username=username,
+        password=password)
+    r = MockRequest()
+    t.addcredentials(r)
+    _check_Authorization_header(r, username, password)
+
+    #   Regression test: Extremely long username & password combinations must
+    # not cause suds to add additional newlines in the constructed
+    # 'Authorization' HTTP header.
+    username = ("An Extremely Long Username that could be usable only to "
+        "Extremely Important People whilst on Extremely Important Missions.")
+    password = ("An Extremely Long Password that could be usable only to "
+        "Extremely Important People whilst on Extremely Important Missions. "
+        "And some extra 'funny' characters to improve security: "
+        "!@#$%^&*():|}|{{.\nEven\nSome\nNewLines\n"
+        "  and spaces at the start of a new line.   ")
+    t = suds.transport.http.HttpAuthenticated(username=username,
+        password=password)
+    r = MockRequest()
+    t.addcredentials(r)
+    _check_Authorization_header(r, username, password)
+
+
+ at pytest.mark.parametrize("url", (
+    "http://my little URL",
+    "https://my little URL",
+    "xxx://my little URL",
+    "xxx:my little URL",
+    "xxx:"))
+def test_http_request_URL(url):
+    """Make sure suds makes a HTTP request targeted at an expected URL."""
+    class MockURLOpener:
+        def open(self, request, timeout=None):
+            assert request.get_full_url() == url
+            raise MyException
+    transport = suds.transport.http.HttpTransport()
+    transport.urlopener = MockURLOpener()
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        transport=transport)
+    pytest.raises(MyException, client.service.f)
+
+
+ at pytest.mark.parametrize("url", (
+    "my no-protocol URL",
+    ":my no-protocol URL"))
+def test_http_request_URL_with_a_missing_protocol_identifier(url):
+    """
+    Test suds reporting URLs with a missing protocol identifier.
+
+    Python urllib library makes this check under Python 3.x, but does not under
+    earlier Python versions.
+
+    """
+    class MockURLOpener:
+        def open(self, request, timeout=None):
+            raise MyException
+    transport = suds.transport.http.HttpTransport()
+    transport.urlopener = MockURLOpener()
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        transport=transport)
+    exceptionClass = ValueError
+    if sys.version_info < (3, 0):
+        exceptionClass = MyException
+    pytest.raises(exceptionClass, client.service.f)
+
+
+def test_sending_unicode_data(monkeypatch):
+    """
+    Original suds implementation passed its request location URL to the
+    underlying HTTP request object as a unicode string.
+
+    Under Python 2.4 this causes no problems as that implementation simply
+    sends all the request data over the network as-is (and treats all unicode
+    data as bytes anyway).
+
+    Under Python 2.7 this causes the httplib HTTP request implementation to
+    convert all of its data to unicode, and do so by simply assuming that data
+    contains only ASCII characters. If any other characters are encountered, it
+    fails with an exception like "UnicodeDecodeError: 'ascii' codec can't
+    decode byte 0xd0 in position 290: ordinal not in range(128)".
+
+    Under Python 3.x the httplib HTTP request implementation automatically
+    converts its received URL to a bytes object (assuming it contains only
+    ASCII characters), thus avoiding the need to convert all the other request
+    data.
+
+    In order to trigger the problematic httplib behaviour we need to make suds
+    attempt to send a HTTP request over the network. On the other hand, we want
+    this test to work even on computers not connected to a network so we
+    monkey-patch the underlying network socket APIs, log all the data suds
+    attempt to send over the network and consider the test run successful once
+    suds attempt to read back data from the network.
+
+    """
+    def callOnce(f):
+        """Method decorator making sure its function only gets called once."""
+        def wrapper(self, *args, **kwargs):
+            fTag = "_%s__%s_called" % (self.__class__.__name__, f.__name__)
+            assert not hasattr(self, fTag)
+            setattr(self, fTag, True)
+            return f(self, *args, **kwargs)
+        return wrapper
+
+    class Mocker:
+        def __init__(self, expectedHost, expectedPort):
+            self.expectedHost = expectedHost
+            self.expectedPort = expectedPort
+            self.sentData = suds.byte_str()
+            self.hostAddress = object()
+        @callOnce
+        def getaddrinfo(self, host, port, *args, **kwargs):
+            assert host == self.expectedHost
+            assert port == self.expectedPort
+            return [(None, None, None, None, self.hostAddress)]
+        @callOnce
+        def socket(self, *args, **kwargs):
+            self.socket = MockSocket(self)
+            return self.socket
+
+    class MockSocketReader:
+        @callOnce
+        def readline(self, *args, **kwargs):
+            raise MyException
+
+    class MockSocket:
+        def __init__(self, mocker):
+            self.__mocker = mocker
+        @callOnce
+        def connect(self, address):
+            assert address is self.__mocker.hostAddress
+        @callOnce
+        def makefile(self, *args, **kwargs):
+            return MockSocketReader()
+        def sendall(self, data):
+            # Python 2.4 urllib implementation calls this function twice - once
+            # for sending the HTTP request headers and once for its body.
+            self.__mocker.sentData += data
+        @callOnce
+        def settimeout(self, *args, **kwargs):
+            assert not hasattr(self, "settimeout_called")
+            self.settimeout_called = True
+
+    host = "an-easily-recognizable-host-name-214894932"
+    port = 9999
+    mocker = Mocker(host, port)
+    monkeypatch.setattr("socket.getaddrinfo", mocker.getaddrinfo)
+    monkeypatch.setattr("socket.socket", mocker.socket)
+    url = "http://%s:%s/svc" % (host, port)
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_input_data(url))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store)
+    data = u"Дмитровский район"
+    pytest.raises(MyException, client.service.f, data)
+    assert data.encode("utf-8") in mocker.sentData
+
+
+def test_sending_non_ascii_location():
+    """
+    Suds should refuse to send HTTP requests with a target location string
+    containing non-ASCII characters. URLs are supposed to consist of
+    characters only.
+
+    """
+    class MockURLOpener:
+        def open(self, request, timeout=None):
+            raise MyException
+    url = u"http://Дмитровский-район-152312306:9999/svc"
+    transport = suds.transport.http.HttpTransport()
+    transport.urlopener = MockURLOpener()
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data(url))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        transport=transport)
+    pytest.raises(UnicodeEncodeError, client.service.f)
+
+
+ at pytest.mark.skipif(sys.version_info >= (3, 0),
+    reason="Python 2 specific functionality")
+ at pytest.mark.parametrize(("urlString", "expectedException"), (
+    ("http://jorgula", MyException),
+    ("http://jorgula_\xe7", UnicodeDecodeError)))
+def test_sending_py2_bytes_location(urlString, expectedException):
+    """
+    Suds should accept single-byte string URL values under Python 2, but should
+    still report an error if those strings contain any non-ASCII characters.
+
+    """
+    class MockURLOpener:
+        def open(self, request, timeout=None):
+            raise MyException
+    transport = suds.transport.http.HttpTransport()
+    transport.urlopener = MockURLOpener()
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data("http://x"))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        transport=transport)
+    client.options.location = suds.byte_str(urlString)
+    pytest.raises(expectedException, client.service.f)
+
+
+ at pytest.mark.skipif(sys.version_info < (3, 0),
+    reason="requires at least Python 3")
+ at pytest.mark.parametrize("urlString", (
+    "http://jorgula",
+    "http://jorgula_\xe7"))
+def test_sending_py3_bytes_location(urlString):
+    """
+    Suds should refuse to send HTTP requests with a target location specified
+    as either a Python 3 bytes or bytearray object.
+
+    """
+    class MockURLOpener:
+        def open(self, request, timeout=None):
+            raise MyException
+    transport = suds.transport.http.HttpTransport()
+    transport.urlopener = MockURLOpener()
+    store = suds.store.DocumentStore(wsdl=_wsdl_with_no_input_data("http://x"))
+    client = suds.client.Client("suds://wsdl", cache=None, documentStore=store,
+        transport=transport)
+
+    expectedException = AssertionError
+    if sys.flags.optimize:
+        expectedException = AttributeError
+
+    for url in (bytes(urlString, encoding="utf-8"),
+        bytearray(urlString, encoding="utf-8")):
+        # Under Python 3.x we can not use the client's 'location' option to set
+        # a bytes URL as it accepts only strings and in Python 3.x all strings
+        # are unicode strings. Therefore, we use an ugly hack, modifying suds's
+        # internal web service description structure to force it to think it
+        # has a bytes object specified as a location for its 'f' web service
+        # operation.
+        client.sd[0].ports[0][0].methods['f'].location = url
+        pytest.raises(expectedException, client.service.f)
+
+
+def _check_Authorization_header(request, username, password):
+    assert len(request.headers) == 1
+    header = request.headers["Authorization"]
+    assert header == _encode_basic_credentials(username, password)
+
+
+def _encode_basic_credentials(username, password):
+    """
+      Encodes user credentials as used in basic HTTP authentication.
+
+      This is the value expected to be added to the 'Authorization' HTTP
+    header.
+
+    """
+    data = suds.byte_str("%s:%s" % (username, password))
+    return "Basic %s" % base64.b64encode(data).decode("utf-8")
+
+
+def _wsdl_with_input_data(url):
+    """
+    Return a WSDL schema with a single operation f taking a single parameter.
+
+    Included operation takes a single string parameter and returns no values.
+    Externally specified URL is used as the web service location.
+
+    """
+    return suds.byte_str(u"""\
+<?xml version="1.0" encoding="utf-8"?>
+<wsdl:definitions targetNamespace="myNamespace"
+  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+  xmlns:tns="myNamespace"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <wsdl:types>
+    <xsd:schema targetNamespace="myNamespace">
+      <xsd:element name="fRequest" type="xsd:string"/>
+    </xsd:schema>
+  </wsdl:types>
+  <wsdl:message name="fInputMessage">
+    <wsdl:part name="parameters" element="tns:fRequest"/>
+  </wsdl:message>
+  <wsdl:portType name="Port">
+    <wsdl:operation name="f">
+      <wsdl:input message="tns:fInputMessage"/>
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="Binding" type="tns:Port">
+    <soap:binding style="document"
+      transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="f">
+      <soap:operation/>
+      <wsdl:input><soap:body use="literal"/></wsdl:input>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="Service">
+    <wsdl:port name="Port" binding="tns:Binding">
+      <soap:address location="%s"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>""" % (url,))
+
+
+def _wsdl_with_no_input_data(url):
+    """
+    Return a WSDL schema with a single operation f taking no parameters.
+
+    Included operation returns no values. Externally specified URL is used as
+    the web service location.
+
+    """
+    return suds.byte_str(u"""\
+<?xml version="1.0" encoding="utf-8"?>
+<wsdl:definitions targetNamespace="myNamespace"
+  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+  xmlns:tns="myNamespace"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <wsdl:portType name="Port">
+    <wsdl:operation name="f"/>
+  </wsdl:portType>
+  <wsdl:binding name="Binding" type="tns:Port">
+    <soap:binding style="document"
+      transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="f"/>
+  </wsdl:binding>
+  <wsdl:service name="Service">
+    <wsdl:port name="Port" binding="tns:Binding">
+      <soap:address location="%s"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>""" % (url,))
-- 
suds



More information about the tryton-debian-vcs mailing list