diff --git a/CHANGES.rst b/CHANGES.rst index d697983..aba1589 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,19 @@ Changelog ========= +.. _changes-2_0_0: + +2.0.0 (not yet released) +~~~~~~~~~~~~~~~~~~~~~~~~ + +Incompatible changes and deprecations +------------------------------------- + ++ `#159`_: Drop support for Python 3.4 and Python 3.5. + +.. _#159: https://github.com/icatproject/python-icat/pull/159 + + .. _changes-1_5_0: 1.5.0 (2024-10-11) diff --git a/doc/src/install.rst b/doc/src/install.rst index 12d0bd4..c5e7ab5 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -14,7 +14,7 @@ System requirements Python ...... -+ 3.4 and newer. ++ 3.6 and newer. Required library packages ......................... diff --git a/python-icat.spec b/python-icat.spec index d0a39b8..3578ed7 100644 --- a/python-icat.spec +++ b/python-icat.spec @@ -16,7 +16,7 @@ Summary: $description License: Apache-2.0 Group: Development/Libraries/Python Source: https://github.com/icatproject/python-icat/releases/download/%{version}/python-icat-%{version}.tar.gz -BuildRequires: python%{pyversfx}-base >= 3.4 +BuildRequires: python%{pyversfx}-base >= 3.6 BuildRequires: python%{pyversfx}-setuptools BuildRequires: fdupes BuildRequires: python-rpm-macros diff --git a/setup.py b/setup.py index 82a2471..f09bc28 100755 --- a/setup.py +++ b/setup.py @@ -176,8 +176,6 @@ def run(self): "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -198,7 +196,7 @@ def run(self): ), packages = ["icat"], package_dir = {"": "src"}, - python_requires = ">=3.4", + python_requires = ">=3.6", install_requires = requires, scripts = [ "src/scripts/icatdump.py", diff --git a/src/icat/chunkedhttp.py b/src/icat/chunkedhttp.py deleted file mode 100644 index 4a7f04d..0000000 --- a/src/icat/chunkedhttp.py +++ /dev/null @@ -1,186 +0,0 @@ -"""HTTP with chunked transfer encoding for urllib. - -.. note:: - This module is included here because python-icat uses it - internally, but it is not considered to be part of the API. - Changes in this module are not considered API changes of - python-icat. It may even be removed from future versions of the - python-icat distribution without further notice. - -This module provides modified versions of HTTPHandler and HTTPSHandler -from urllib. These handlers differ from the standard counterparts in -that they are able to send the data using chunked transfer encoding to -the HTTP server. - -Note that although the handlers are designed as drop in replacements -for the standard counterparts, we do not intent to catch all corner -cases and be fully compatible in all situations. The implementations -here shall be just good enough for the use cases in IDSClient. - -Starting with Python 3.6.0, support for chunked transfer encoding has -been added to the standard library, see `Issue 12319`_. As a result, -this module is obsolete for newer Python versions and python-icat will -use it only for older versions. - -.. _Issue 12319: https://bugs.python.org/issue12319 -""" - -import http.client -import urllib.error -import urllib.request - - -# We always set the Content-Length header for these methods because some -# servers will otherwise respond with a 411 -_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'} - -def stringiterator(buffer): - """Wrap a string in an iterator that yields it in one single chunk.""" - if len(buffer) > 0: - yield buffer - -def fileiterator(f, chunksize=8192): - """Yield the content of a file by chunks of a given size at a time.""" - while True: - chunk = f.read(chunksize) - if not chunk: - break - yield chunk - -class HTTPConnectionMixin: - """Implement chunked transfer encoding in HTTP. - - This is designed as a mixin class to modify either HTTPConnection - or HTTPSConnection accordingly. - """ - - def _send_request(self, method, url, body, headers): - # This method is taken and modified from the Python 2.7 - # httplib.py to prevent it from trying to set a Content-length - # header and to hook in our send_body() method. - # Admitted, it's an evil hack. - header_names = {k.lower(): k for k in headers.keys()} - skips = {} - if 'host' in header_names: - skips['skip_host'] = 1 - if 'accept-encoding' in header_names: - skips['skip_accept_encoding'] = 1 - - self.putrequest(method, url, **skips) - - chunked = False - if 'transfer-encoding' in header_names: - if headers[header_names['transfer-encoding']] == 'chunked': - chunked = True - else: - raise http.client.HTTPException("Invalid Transfer-Encoding") - - for hdr, value in headers.items(): - self.putheader(hdr, value) - self.endheaders() - self.send_body(body, chunked) - - def send_body(self, body, chunked): - """Send the body, either as is or chunked. - - The empty line separating the headers from the body must have - been sent before calling this method. - """ - if body is not None: - if isinstance(body, bytes): - bodyiter = stringiterator(body) - elif isinstance(body, str): - bodyiter = stringiterator(body.encode('ascii')) - elif hasattr(body, 'read'): - bodyiter = fileiterator(body) - elif hasattr(body, '__iter__'): - bodyiter = body - else: - raise TypeError("expect either a string, a file, " - "or an iterable") - if chunked: - for chunk in bodyiter: - self.send(hex(len(chunk))[2:].encode('ascii') - + b"\r\n" + chunk + b"\r\n") - self.send(b"0\r\n\r\n") - else: - for chunk in bodyiter: - self.send(chunk) - -class HTTPConnection(HTTPConnectionMixin, http.client.HTTPConnection): - pass - -class HTTPSConnection(HTTPConnectionMixin, http.client.HTTPSConnection): - pass - - -class HTTPHandlerMixin: - """Internal helper class. - - This is designed as a mixin class to modify either HTTPHandler or - HTTPSHandler accordingly. It overrides do_request_() inherited - from AbstractHTTPHandler. - """ - - def do_request_(self, request): - # The original method from AbstractHTTPHandler sets some - # defaults that are unsuitable for our use case. In - # particular it tries to enforce Content-length to be set (and - # fails doing so if data is not a string), while for chunked - # transfer encoding Content-length must not be set. - - if not request.host: - raise urllib.error.URLError('no host given') - - if request.data is not None: - if not request.has_header('Content-type'): - raise urllib.error.URLError('no Content-type header given') - if not request.has_header('Content-length'): - if isinstance(request.data, (bytes, str)): - request.add_unredirected_header( - 'Content-length', '%d' % len(request.data)) - else: - request.add_unredirected_header( - 'Transfer-Encoding', 'chunked') - else: - if request.get_method().upper() in _METHODS_EXPECTING_BODY: - request.add_unredirected_header('Content-length', '0') - - sel_host = request.host - if request.has_proxy(): - scheme, sel = splittype(request.selector) - sel_host, sel_path = splithost(sel) - if not request.has_header('Host'): - request.add_unredirected_header('Host', sel_host) - for name, value in self.parent.addheaders: - name = name.capitalize() - if not request.has_header(name): - request.add_unredirected_header(name, value) - - return request - -class HTTPHandler(HTTPHandlerMixin, urllib.request.HTTPHandler): - - def http_open(self, req): - return self.do_open(HTTPConnection, req) - - http_request = HTTPHandlerMixin.do_request_ - -class HTTPSHandler(HTTPHandlerMixin, urllib.request.HTTPSHandler): - - def https_open(self, req): - if hasattr(self, '_context') and hasattr(self, '_check_hostname'): - # Python 3.2 and newer - return self.do_open(HTTPSConnection, req, - context=self._context, - check_hostname=self._check_hostname) - elif hasattr(self, '_context'): - # Python 2.7.9 - return self.do_open(HTTPSConnection, req, - context=self._context) - else: - # Python 2.7.8 or 3.1 and older - return self.do_open(HTTPSConnection, req) - - https_request = HTTPHandlerMixin.do_request_ - diff --git a/src/icat/config.py b/src/icat/config.py index 992fd96..3b63bfa 100644 --- a/src/icat/config.py +++ b/src/icat/config.py @@ -15,14 +15,6 @@ __all__ = ['boolean', 'flag', 'Configuration', 'Config'] -# Evil hack: Path.expanduser() has been added in Python 3.5. -# Monkeypatch the class for older Python versions. -if not hasattr(Path, "expanduser"): - import os.path - def _expanduser(p): - return Path(os.path.expanduser(str(p))) - Path.expanduser = _expanduser - if sys.platform.startswith("win"): cfgdirs = [ Path(os.environ['ProgramData'], "ICAT"), diff --git a/src/icat/ids.py b/src/icat/ids.py index 42ee136..075e478 100644 --- a/src/icat/ids.py +++ b/src/icat/ids.py @@ -16,23 +16,14 @@ import sys from urllib.error import HTTPError from urllib.parse import urlencode -from urllib.request import HTTPDefaultErrorHandler, ProxyHandler, Request -from urllib.request import build_opener +from urllib.request import HTTPHandler, HTTPSHandler, HTTPDefaultErrorHandler +from urllib.request import ProxyHandler, Request, build_opener import zlib from .entity import Entity from .exception import * from .helper import Version -# For Python versions older then 3.6.0b1, the standard library does -# not support sending the body using chunked transfer encoding. Need -# to replace the HTTPHandler with our modified versions from -# icat.chunkedhttp in this case. -if sys.version_info < (3, 6, 0, 'beta'): - from .chunkedhttp import HTTPHandler, HTTPSHandler -else: - from urllib.request import HTTPHandler, HTTPSHandler - __all__ = ['DataSelection', 'IDSClient']