diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 7ce9369a3..c107a2010 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.0.0
+current_version = 4.1.0
commit = true
tag = true
tag_name = {new_version}
diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml
index 61b9abab4..6ee31cbc3 100644
--- a/.github/workflows/python-test.yml
+++ b/.github/workflows/python-test.yml
@@ -9,7 +9,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
@@ -20,10 +20,14 @@ jobs:
test:
runs-on: ${{ matrix.platform }}
strategy:
+ fail-fast: false
max-parallel: 4
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.6", "3.7", "3.8"]
+ python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+ # TODO: Remove Windows exclusion when binary wheel available for lxml
+ exclude:
+ - { platform: windows-latest, python-version: "3.10" }
steps:
- name: Install system dependencies
@@ -37,7 +41,7 @@ jobs:
brew install libxmlsec1 libxslt pkgconfig
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -65,7 +69,7 @@ jobs:
name: coverage-data
path: .
- name: Set up Python 3.7
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
diff --git a/CHANGES b/CHANGES
index a73f9a635..37d54d591 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,14 @@
+4.1.0 (2021-08-15)
+------------------
+ - Remove last dependency on `six` (#1250)
+ - Use `platformdirs` instead of the `appsdirs` dependency (#1244)
+ - Pass digest method when signing timestamp node(#1201)
+ - Fix settings context manager when an exception is raised (#1193)
+ - Don't render decimals using scientific notation (#1191)
+ - Remove dependency on `defusedxml` (deprecated) (#1179)
+ - Improve handling of str values for Duration (#1165)
+
+
4.0.0 (2020-10-12)
------------------
- Drop support for Python 2.7, 3.3, 3.4 and 3.5
diff --git a/LICENSE b/LICENSE
index 0dd79b773..db6c7c6a7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016-2017 Michael van Tellingen
+Copyright (c) 2016-2021 Michael van Tellingen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MANIFEST.in b/MANIFEST.in
index 2f6e6ce75..d96cc94ce 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -7,6 +7,7 @@ include CHANGES
include CONTRIBUTORS.rst
include LICENSE
include README.rst
+include pyproject.toml
include setup.cfg
include setup.py
diff --git a/README.rst b/README.rst
index 16e1c6a6d..6a05eca49 100644
--- a/README.rst
+++ b/README.rst
@@ -5,7 +5,7 @@ Zeep: Python SOAP client
A fast and modern Python SOAP client
Highlights:
- * Compatible with Python 3.6, 3.7, 3.8 and PyPy
+ * Compatible with Python 3.6, 3.7, 3.8, 3.9, 3.10 and PyPy3
* Build on top of lxml and requests
* Support for Soap 1.1, Soap 1.2 and HTTP bindings
* Support for WS-Addressing headers
diff --git a/docs/conf.py b/docs/conf.py
index 6d785ed70..f006c1af0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -62,7 +62,7 @@
# built documents.
#
# The short X.Y version.
-version = '4.0.0'
+version = '4.1.0'
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
diff --git a/docs/index.rst b/docs/index.rst
index 260ae88f9..b32949c9a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -106,7 +106,7 @@ See ``python -mzeep --help`` for more information about this command.
.. note:: Zeep follows `semver`_ for versioning, however bugs can always occur.
So as always pin the version of zeep you tested with
- (e.g. ``zeep==4.0.0``').
+ (e.g. ``zeep==4.1.0``').
.. _semver: http://semver.org/
diff --git a/docs/plugins.rst b/docs/plugins.rst
index 5dbf434a0..d6e37c7ca 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -27,7 +27,10 @@ Writing a plugin is really simple and best explained via an example.
The plugin can implement two methods: ``ingress`` and ``egress``. Both methods
-should always return an envelop (lxml element) and the http headers.
+should always return an envelop (lxml element) and the http headers. The
+envelope in the ``egress`` plugin will only contain the body of the soap message.
+This is important to remember if you want to inspect or do something
+with the headers.
To register this plugin you need to pass it to the client. Plugins are always
executed sequentially.
diff --git a/setup.py b/setup.py
index 9f5f4d48b..4cf9073b9 100755
--- a/setup.py
+++ b/setup.py
@@ -3,11 +3,11 @@
from setuptools import setup
install_requires = [
- "appdirs>=1.4.0",
"attrs>=17.2.0",
- "cached-property>=1.3.0",
+ "cached-property>=1.3.0; python_version<'3.8'",
"isodate>=0.5.4",
"lxml>=4.6.0",
+ "platformdirs>=1.4.0",
"requests>=2.7.0",
"requests-toolbelt>=0.7.1",
"requests-file>=1.5.1",
@@ -27,12 +27,11 @@
tests_require = [
"coverage[toml]==5.2.1",
"freezegun==0.3.15",
- "mock==2.0.0",
"pretend==1.0.9",
"pytest-cov==2.8.1",
"pytest-httpx",
"pytest-asyncio",
- "pytest==6.0.1",
+ "pytest==6.2.5",
"requests_mock>=0.7.0",
# Linting
"isort==5.3.2",
@@ -50,12 +49,12 @@
setup(
name="zeep",
- version="4.0.0",
+ version="4.1.0",
description="A modern/fast Python SOAP client based on lxml / requests",
long_description=long_description,
author="Michael van Tellingen",
author_email="michaelvantellingen@gmail.com",
- url="http://docs.python-zeep.org",
+ url="https://docs.python-zeep.org",
python_requires=">=3.6",
install_requires=install_requires,
tests_require=tests_require,
@@ -74,10 +73,12 @@
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
diff --git a/src/zeep/__init__.py b/src/zeep/__init__.py
index a179b9872..eee2f6fb6 100644
--- a/src/zeep/__init__.py
+++ b/src/zeep/__init__.py
@@ -4,4 +4,4 @@
from zeep.transports import Transport # noqa
from zeep.xsd.valueobjects import AnyObject # noqa
-__version__ = "4.0.0"
+__version__ = "4.1.0"
diff --git a/src/zeep/cache.py b/src/zeep/cache.py
index eebb39d12..e484cdc97 100644
--- a/src/zeep/cache.py
+++ b/src/zeep/cache.py
@@ -7,7 +7,7 @@
import typing
from contextlib import contextmanager
-import appdirs
+import platformdirs
import pytz
# The sqlite3 is not available on Google App Engine so we handle the
@@ -176,7 +176,7 @@ def _is_expired(value, timeout):
def _get_default_cache_path():
- path = appdirs.user_cache_dir("zeep", False)
+ path = platformdirs.user_cache_dir("zeep", False)
try:
os.makedirs(path)
except OSError as exc:
diff --git a/src/zeep/loader.py b/src/zeep/loader.py
index 69723fd75..9bf979acd 100644
--- a/src/zeep/loader.py
+++ b/src/zeep/loader.py
@@ -2,11 +2,10 @@
import typing
from urllib.parse import urljoin, urlparse, urlunparse
-from exceptions import DTDForbidden, EntitiesForbidden
from lxml import etree
-from lxml.etree import fromstring, XMLParser, XMLSyntaxError, Resolver
+from lxml.etree import Resolver, XMLParser, XMLSyntaxError, fromstring
-from zeep.exceptions import XMLSyntaxError
+from zeep.exceptions import DTDForbidden, EntitiesForbidden, XMLSyntaxError
from zeep.settings import Settings
@@ -48,11 +47,13 @@ def parse_xml(content: str, transport, base_url=None, settings=None):
)
parser.resolvers.add(ImportResolver(transport))
try:
- elementtree = fromstring(content, parser=parser,base_url=base_url)
+ elementtree = fromstring(content, parser=parser, base_url=base_url)
docinfo = elementtree.getroottree().docinfo
if docinfo.doctype:
if settings.forbid_dtd:
- raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id)
+ raise DTDForbidden(
+ docinfo.doctype, docinfo.system_url, docinfo.public_id
+ )
if settings.forbid_entities:
for dtd in docinfo.internalDTD, docinfo.externalDTD:
if dtd is None:
@@ -60,7 +61,6 @@ def parse_xml(content: str, transport, base_url=None, settings=None):
for entity in dtd.iterentities():
raise EntitiesForbidden(entity.name, entity.content)
-
return elementtree
except etree.XMLSyntaxError as exc:
raise XMLSyntaxError(
diff --git a/src/zeep/proxy.py b/src/zeep/proxy.py
index 0b5e7ea7f..bbcd6377a 100644
--- a/src/zeep/proxy.py
+++ b/src/zeep/proxy.py
@@ -99,11 +99,11 @@ def __getitem__(self, key):
raise AttributeError("Service has no operation %r" % key)
def __iter__(self):
- """ Return iterator over the services and their callables. """
+ """Return iterator over the services and their callables."""
return iter(self._operations.items())
def __dir__(self):
- """ Return the names of the operations. """
+ """Return the names of the operations."""
return list(itertools.chain(dir(super()), self._operations))
diff --git a/src/zeep/transports.py b/src/zeep/transports.py
index 70185d82f..0841e2f16 100644
--- a/src/zeep/transports.py
+++ b/src/zeep/transports.py
@@ -37,6 +37,7 @@ def __init__(self, cache=None, timeout=300, operation_timeout=None, session=None
self.operation_timeout = operation_timeout
self.logger = logging.getLogger(__name__)
+ self.__close_session = not session
self.session = session or requests.Session()
self.session.mount("file://", FileAdapter())
self.session.headers["User-Agent"] = "Zeep/%s (www.python-zeep.org)" % (
@@ -154,6 +155,10 @@ def settings(self, timeout=None):
yield
self.operation_timeout = old_timeout
+ def __del__(self):
+ if self.__close_session:
+ self.session.close()
+
class AsyncTransport(Transport):
"""Asynchronous Transport class using httpx.
@@ -170,7 +175,6 @@ def __init__(
cache=None,
timeout=300,
operation_timeout=None,
- session=None,
verify_ssl=True,
proxy=None,
):
diff --git a/src/zeep/wsdl/attachments.py b/src/zeep/wsdl/attachments.py
index 037e43906..075bee591 100644
--- a/src/zeep/wsdl/attachments.py
+++ b/src/zeep/wsdl/attachments.py
@@ -6,7 +6,11 @@
import base64
-from cached_property import cached_property
+try:
+ from functools import cached_property
+except ImportError:
+ from cached_property import cached_property
+
from requests.structures import CaseInsensitiveDict
diff --git a/src/zeep/wsdl/bindings/soap.py b/src/zeep/wsdl/bindings/soap.py
index 44cd44034..81b70b091 100644
--- a/src/zeep/wsdl/bindings/soap.py
+++ b/src/zeep/wsdl/bindings/soap.py
@@ -322,7 +322,7 @@ def process_error(self, doc, operation):
)
def get_text(name):
- child = fault_node.find(name)
+ child = fault_node.find(name, namespaces=fault_node.nsmap)
if child is not None:
return child.text
@@ -330,7 +330,7 @@ def get_text(name):
message=get_text("faultstring"),
code=get_text("faultcode"),
actor=get_text("faultactor"),
- detail=fault_node.find("detail"),
+ detail=fault_node.find("detail", namespaces=fault_node.nsmap),
)
def _set_http_headers(self, serialized, operation):
diff --git a/src/zeep/wsse/signature.py b/src/zeep/wsse/signature.py
index 143ca0f5f..c4aec758e 100644
--- a/src/zeep/wsse/signature.py
+++ b/src/zeep/wsse/signature.py
@@ -244,7 +244,7 @@ def _signature_prepare(envelope, key, signature_method, digest_method):
_sign_node(ctx, signature, envelope.find(QName(soap_env, "Body")), digest_method)
timestamp = security.find(QName(ns.WSU, "Timestamp"))
if timestamp != None:
- _sign_node(ctx, signature, timestamp)
+ _sign_node(ctx, signature, timestamp, digest_method)
ctx.sign(signature)
# Place the X509 data inside a WSSE SecurityTokenReference within
diff --git a/src/zeep/wsse/username.py b/src/zeep/wsse/username.py
index bb9b4cd04..da448b445 100644
--- a/src/zeep/wsse/username.py
+++ b/src/zeep/wsse/username.py
@@ -2,8 +2,6 @@
import hashlib
import os
-import six
-
from zeep import ns
from zeep.wsse import utils
@@ -108,7 +106,7 @@ def _create_password_digest(self):
nonce = os.urandom(16)
timestamp = utils.get_timestamp(self.created, self.zulu_timestamp)
- if isinstance(self.password, six.string_types):
+ if isinstance(self.password, str):
password = self.password.encode("utf-8")
else:
password = self.password
diff --git a/src/zeep/xsd/elements/indicators.py b/src/zeep/xsd/elements/indicators.py
index 40325dad2..e9ef2c4d3 100644
--- a/src/zeep/xsd/elements/indicators.py
+++ b/src/zeep/xsd/elements/indicators.py
@@ -16,7 +16,11 @@
import typing
from collections import OrderedDict, defaultdict, deque
-from cached_property import threaded_cached_property
+try:
+ from functools import cached_property as threaded_cached_property
+except ImportError:
+ from cached_property import threaded_cached_property
+
from lxml import etree
from zeep.exceptions import UnexpectedElementError, ValidationError
diff --git a/src/zeep/xsd/types/any.py b/src/zeep/xsd/types/any.py
index b4525e44c..17f244e1d 100644
--- a/src/zeep/xsd/types/any.py
+++ b/src/zeep/xsd/types/any.py
@@ -1,7 +1,11 @@
import logging
import typing
-from cached_property import threaded_cached_property
+try:
+ from functools import cached_property as threaded_cached_property
+except ImportError:
+ from cached_property import threaded_cached_property
+
from lxml import etree
from zeep.utils import qname_attr
diff --git a/src/zeep/xsd/types/builtins.py b/src/zeep/xsd/types/builtins.py
index e3e05c657..d57068eea 100644
--- a/src/zeep/xsd/types/builtins.py
+++ b/src/zeep/xsd/types/builtins.py
@@ -124,10 +124,12 @@ def pythonvalue(self, value):
class Duration(BuiltinType):
_default_qname = xsd_ns("duration")
- accepted_types = [isodate.duration.Duration, str]
+ accepted_types = [isodate.duration.Duration, datetime.timedelta, str]
@check_no_collection
def xmlvalue(self, value):
+ if isinstance(value, str):
+ value = isodate.parse_duration(value)
return isodate.duration_isoformat(value)
@treat_whitespace("collapse")
diff --git a/src/zeep/xsd/types/complex.py b/src/zeep/xsd/types/complex.py
index 8141bc168..b2ed9bf0c 100644
--- a/src/zeep/xsd/types/complex.py
+++ b/src/zeep/xsd/types/complex.py
@@ -4,7 +4,11 @@
from collections import OrderedDict, deque
from itertools import chain
-from cached_property import threaded_cached_property
+try:
+ from functools import cached_property as threaded_cached_property
+except ImportError:
+ from cached_property import threaded_cached_property
+
from lxml import etree
from zeep.exceptions import UnexpectedElementError, XMLParseError
diff --git a/tests/conftest.py b/tests/conftest.py
index 64c9bf951..4bd41de06 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,10 +2,6 @@
import pytest
-# Don't try to test asyncio since it is py3 only syntax
-if sys.version_info < (3, 5):
- collect_ignore = ["test_asyncio_transport.py"]
-
pytest.register_assert_rewrite("tests.utils")
diff --git a/tests/test_async_transport.py b/tests/test_async_transport.py
index 940b48a3c..f5e8d1b0a 100644
--- a/tests/test_async_transport.py
+++ b/tests/test_async_transport.py
@@ -1,6 +1,4 @@
-import aiohttp
import pytest
-from aioresponses import aioresponses
from lxml import etree
from pretend import stub
from pytest_httpx import HTTPXMock
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 4ab54559d..c50d8d0e0 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -1,8 +1,8 @@
import pytest
-from exceptions import DTDForbidden, EntitiesForbidden
from pytest import raises as assert_raises
from tests.utils import DummyTransport
+from zeep.exceptions import DTDForbidden, EntitiesForbidden
from zeep.loader import parse_xml
from zeep.settings import Settings
diff --git a/tests/test_main.py b/tests/test_main.py
index bffc7230f..f45048202 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,4 +1,5 @@
-from mock import patch
+from unittest.mock import patch
+
from pretend import stub
from zeep import __main__, client
diff --git a/tests/test_wsdl.py b/tests/test_wsdl.py
index 27cbe0fa0..6edfd14da 100644
--- a/tests/test_wsdl.py
+++ b/tests/test_wsdl.py
@@ -3,12 +3,12 @@
import pytest
import requests_mock
-from exceptions import DTDForbidden, EntitiesForbidden
from lxml import etree
from pretend import stub
from tests.utils import DummyTransport, assert_nodes_equal
from zeep import Client, Settings, wsdl
+from zeep.exceptions import DTDForbidden, EntitiesForbidden
from zeep.transports import Transport
diff --git a/tests/test_wsdl_soap.py b/tests/test_wsdl_soap.py
index 7a2810899..b8a40b28c 100644
--- a/tests/test_wsdl_soap.py
+++ b/tests/test_wsdl_soap.py
@@ -62,6 +62,39 @@ def test_soap11_process_error():
assert exc.subcodes is None
assert "detail-message" in etree.tostring(exc.detail).decode("utf-8")
+ responseWithNamespaceInFault = load_xml(
+ """
+
+
+
+ fault-code-withNamespace
+ fault-string-withNamespace
+
+
+ detail-message-withNamespace
+ detail-code-withNamespace
+
+
+
+
+
+ """
+ )
+
+ try:
+ binding.process_error(responseWithNamespaceInFault, None)
+ assert False
+ except Fault as exc:
+ assert exc.message == "fault-string-withNamespace"
+ assert exc.code == "fault-code-withNamespace"
+ assert exc.actor is None
+ assert exc.subcodes is None
+ assert "detail-message-withNamespace" in etree.tostring(exc.detail).decode(
+ "utf-8"
+ )
+
def test_soap12_process_error():
response = """
diff --git a/tests/test_wsse_signature.py b/tests/test_wsse_signature.py
index 2f3e730eb..ae3c353f7 100644
--- a/tests/test_wsse_signature.py
+++ b/tests/test_wsse_signature.py
@@ -35,7 +35,16 @@
@skip_if_no_xmlsec
-def test_sign_timestamp_if_present():
+@pytest.mark.parametrize("digest_method,expected_digest_href", DIGEST_METHODS_TESTDATA)
+@pytest.mark.parametrize(
+ "signature_method,expected_signature_href", SIGNATURE_METHODS_TESTDATA
+)
+def test_sign_timestamp_if_present(
+ digest_method,
+ signature_method,
+ expected_digest_href,
+ expected_signature_href,
+):
envelope = load_xml(
"""