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( """