From 420dc05543ac0915d8597026f1aacac810fffdb9 Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 12:45:58 -0700 Subject: [PATCH 1/7] Harden cleanup during interpreter shutdown --- riak/transports/pool.py | 16 ++++++++++++---- riak/transports/tcp/connection.py | 17 ++++++++++++++--- riak/util.py | 2 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/riak/transports/pool.py b/riak/transports/pool.py index 38a87b43..b1bd1d5d 100644 --- a/riak/transports/pool.py +++ b/riak/transports/pool.py @@ -15,6 +15,7 @@ from __future__ import print_function import threading +import sys from contextlib import contextmanager @@ -231,8 +232,15 @@ def clear(self): Removes all resources from the pool, calling :meth:`delete_resource` with each one so that the resources are cleaned up. """ - for resource in self: - self.delete_resource(resource) + try: + for resource in self: + self.delete_resource(resource) + except: + if sys.exc_info()[1].__str__().find('StopIteration') < 0: + raise + # Annoying issue happening during interpreter shutdown with + # apache wsgi module. + pass def create_resource(self): """ @@ -279,9 +287,9 @@ def __iter__(self): def next(self): # Python 2.x version - if len(self.targets) == 0: + if self.targets.__len__() == 0: raise StopIteration - if len(self.unlocked) == 0: + if self.unlocked.__len__() == 0: self.__claim_resources() return self.unlocked.pop(0) diff --git a/riak/transports/tcp/connection.py b/riak/transports/tcp/connection.py index 13c02cf4..cdef8b33 100644 --- a/riak/transports/tcp/connection.py +++ b/riak/transports/tcp/connection.py @@ -16,6 +16,7 @@ import logging import socket import struct +import sys import six import riak.pb.riak_pb2 import riak.pb.messages @@ -273,11 +274,21 @@ def close(self): # shutdown() method due to the SSL lib try: self._socket.shutdown(socket.SHUT_RDWR) - except EnvironmentError: + except Exception as why: # NB: sometimes these exceptions are raised if the initial # connection didn't succeed correctly, or if shutdown() is # called after the connection dies - logging.debug('Exception occurred while shutting ' - 'down socket.', exc_info=True) + if isinstance(why, OSError) and why.errno == errno.EBADF: + pass + try: + logging.debug('Exception occurred while shutting ' + 'down socket.', exc_info=True) + except: + if sys.exc_info()[1].__str__().find('is not defined') < 0: + raise + # Annoying issue happening during interpreter shutdown with + # apache wsgi module. + pass + self._socket.close() del self._socket diff --git a/riak/util.py b/riak/util.py index 9101275b..d762d7bd 100644 --- a/riak/util.py +++ b/riak/util.py @@ -110,7 +110,7 @@ def __get__(self, obj, cls): if obj is None: return None value = self.fget(obj) - setattr(obj, self.func_name, value) + obj.__setattr__(self.func_name, value) return value From 8860d930f7bfc093baf876139ffb66354e5d64d6 Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 12:46:02 -0700 Subject: [PATCH 2/7] Add value equality for vector clocks --- riak/riak_object.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/riak/riak_object.py b/riak/riak_object.py index ab9650ca..d86d0b9c 100644 --- a/riak/riak_object.py +++ b/riak/riak_object.py @@ -98,6 +98,8 @@ def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, self.encode('base64')) + def __eq__(self, other): + return (self._vclock == other._vclock) class RiakObject(object): """ From bdebaef83f42405cded31144a8ec74a62fbbde5c Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 12:46:02 -0700 Subject: [PATCH 3/7] Use collections ABCs on modern Python --- riak/client/index_page.py | 3 ++- riak/datatypes/map.py | 2 +- riak/datatypes/register.py | 2 +- riak/datatypes/set.py | 6 +++--- riak/mapreduce.py | 3 ++- riak/util.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/riak/client/index_page.py b/riak/client/index_page.py index 8e094a66..308d89b0 100644 --- a/riak/client/index_page.py +++ b/riak/client/index_page.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import namedtuple, Sequence +from collections import namedtuple +from collections.abc import Sequence CONTINUATION = namedtuple('Continuation', ['c']) diff --git a/riak/datatypes/map.py b/riak/datatypes/map.py index b5b790bf..a4326b63 100644 --- a/riak/datatypes/map.py +++ b/riak/datatypes/map.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import Mapping +from collections.abc import Mapping from riak.util import lazy_property from .datatype import Datatype from riak.datatypes import TYPES diff --git a/riak/datatypes/register.py b/riak/datatypes/register.py index 247a2a52..5593b998 100644 --- a/riak/datatypes/register.py +++ b/riak/datatypes/register.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections import Sized +from collections.abc import Sized from riak.datatypes.datatype import Datatype from six import string_types from riak.datatypes import TYPES diff --git a/riak/datatypes/set.py b/riak/datatypes/set.py index 19829cf3..7b400b9b 100644 --- a/riak/datatypes/set.py +++ b/riak/datatypes/set.py @@ -21,7 +21,7 @@ __all__ = ['Set'] -class Set(collections.Set, Datatype): +class Set(collections.abc.Set, Datatype): """A convergent datatype representing a Set with observed-remove semantics. Currently strings are the only supported value type. Example:: @@ -72,7 +72,7 @@ def to_op(self): changes['removes'] = list(self._removes) return changes - # collections.Set API, operates only on the immutable version + # collections.abc.Set API, operates only on the immutable version def __contains__(self, element): return element in self.value @@ -116,7 +116,7 @@ def _coerce_value(self, new_value): return frozenset(new_value) def _check_type(self, new_value): - if not isinstance(new_value, collections.Iterable): + if not isinstance(new_value, collections.abc.Iterable): return False for element in new_value: if not isinstance(element, string_types): diff --git a/riak/mapreduce.py b/riak/mapreduce.py index 1b604663..9a11d813 100644 --- a/riak/mapreduce.py +++ b/riak/mapreduce.py @@ -16,7 +16,8 @@ # limitations under the License. from __future__ import print_function -from collections import Iterable, namedtuple +from collections import namedtuple +from collections.abc import Iterable from six import string_types, PY2 import riak diff --git a/riak/util.py b/riak/util.py index d762d7bd..9310caec 100644 --- a/riak/util.py +++ b/riak/util.py @@ -18,7 +18,7 @@ import sys import warnings -from collections import Mapping +from collections.abc import Mapping from six import string_types, PY2 epoch = datetime.datetime.utcfromtimestamp(0) From 20cb13b2e83c0a17f1ad34a3d3c71226172fe8ba Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 12:48:44 -0700 Subject: [PATCH 4/7] Replace removed cgi header parser --- riak/codecs/http.py | 7 +++---- riak/tests/test_util.py | 12 ++++++++++++ riak/transports/http/stream.py | 5 ++--- riak/util.py | 11 +++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/riak/codecs/http.py b/riak/codecs/http.py index b981b77a..a7a7e185 100644 --- a/riak/codecs/http.py +++ b/riak/codecs/http.py @@ -16,7 +16,6 @@ import csv import six -from cgi import parse_header from email import message_from_string from email.utils import parsedate_tz, mktime_tz from xml.etree import ElementTree @@ -25,7 +24,7 @@ from riak.riak_object import VClock from riak.multidict import MultiDict from riak.transports.http.search import XMLSearchResult -from riak.util import decode_index_value, bytes_to_str +from riak.util import decode_index_value, bytes_to_str, parse_http_header if six.PY2: from urllib import unquote_plus @@ -75,7 +74,7 @@ def _parse_body(self, robj, response, expected_statuses): robj.key = headers['location'].strip().split('/')[-1] # If 300(Siblings), apply the siblings to the object elif status == 300: - ctype, params = parse_header(headers['content-type']) + ctype, params = parse_http_header(headers['content-type']) if ctype == 'multipart/mixed': if six.PY3: data = bytes_to_str(data) @@ -269,7 +268,7 @@ def _parse_content_type(self, value): :param value: Complete MIME content-type string """ - content_type, params = parse_header(value) + content_type, params = parse_http_header(value) if 'charset' in params: charset = params['charset'] else: diff --git a/riak/tests/test_util.py b/riak/tests/test_util.py index 766c82fa..01215848 100644 --- a/riak/tests/test_util.py +++ b/riak/tests/test_util.py @@ -17,10 +17,22 @@ from riak.util import is_timeseries_supported, \ datetime_from_unix_time_millis, \ + parse_http_header, \ unix_time_millis class UtilUnitTests(unittest.TestCase): + def test_parse_http_header_without_parameters(self): + self.assertEqual(('text/plain', {}), + parse_http_header('text/plain')) + + def test_parse_http_header_with_parameters(self): + value = 'multipart/mixed; boundary="semi;colon"; charset=UTF-8' + self.assertEqual( + ('multipart/mixed', + {'boundary': 'semi;colon', 'charset': 'UTF-8'}), + parse_http_header(value)) + # NB: # 144379690 secs, 987 msecs past epoch # 144379690987 total msecs past epoch diff --git a/riak/transports/http/stream.py b/riak/transports/http/stream.py index 590565f2..678cae9c 100644 --- a/riak/transports/http/stream.py +++ b/riak/transports/http/stream.py @@ -15,9 +15,8 @@ import json import re -from cgi import parse_header from email import message_from_string -from riak.util import decode_index_value +from riak.util import decode_index_value, parse_http_header from riak.client.index_page import CONTINUATION from riak import RiakError from six import PY2 @@ -110,7 +109,7 @@ class HttpMultipartStream(HttpStream): def __init__(self, response): super(HttpMultipartStream, self).__init__(response) ctypehdr = response.getheader('content-type') - _, params = parse_header(ctypehdr) + _, params = parse_http_header(ctypehdr) self.boundary_re = re.compile('\r?\n--%s(?:--)?\r?\n' % re.escape(params['boundary'])) self.next_boundary = None diff --git a/riak/util.py b/riak/util.py index 9310caec..cb3e952f 100644 --- a/riak/util.py +++ b/riak/util.py @@ -19,6 +19,7 @@ import warnings from collections.abc import Mapping +from email.message import Message from six import string_types, PY2 epoch = datetime.datetime.utcfromtimestamp(0) @@ -60,6 +61,16 @@ def quacks_like_dict(object): return isinstance(object, Mapping) +def parse_http_header(value): + """Parse a MIME-style HTTP header value and its parameters.""" + message = Message() + message['content-type'] = value + params = message.get_params(header='content-type', unquote=True) + if not params: + return value, {} + return params[0][0], dict(params[1:]) + + def deep_merge(a, b): """Merge two deep dicts non-destructively From 0e7a8c2f339beae88b261b0e7fada6c8447a33e7 Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 15:09:01 -0700 Subject: [PATCH 5/7] Remove modules that aren't needed. --- .gitmodules | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitmodules b/.gitmodules index 510fba6e..786c7044 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "riak_pb"] - path = riak_pb - url = git://github.com/basho/riak_pb.git -[submodule "tools"] - path = tools - url = git://github.com/basho/riak-client-tools.git [submodule "docs"] path = docs url = https://github.com/basho/riak-python-client.git From 3e98eec5175a57cfe54ac2dd8796b161de2592c5 Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 15:15:59 -0700 Subject: [PATCH 6/7] Support untagged source builds --- commands.py | 2 +- version.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/commands.py b/commands.py index a20557ab..d4f63d20 100644 --- a/commands.py +++ b/commands.py @@ -398,7 +398,7 @@ def _format_python2_or_3(self): # class RpbCounterGetResp(_message.Message): contents = re.sub( r'class\s+(\S+)\((\S+)\):\s*\n' - '\s+__metaclass__\s+=\s+(\S+)\s*\n', + r'\s+__metaclass__\s+=\s+(\S+)\s*\n', r'@add_metaclass(\3)\nclass \1(\2):\n', contents) with open(im, 'w', buffering=1) as pbfile: diff --git a/version.py b/version.py index ca6a019c..20d56745 100644 --- a/version.py +++ b/version.py @@ -81,10 +81,15 @@ def get_version(): # Get the version using "git describe". cmd = 'git describe --tags --match [0-9]*'.split() try: - version = check_output(cmd).decode().strip() + version = check_output(cmd, cwd=d or '.', + stderr=PIPE).decode().strip() except CalledProcessError: - print('Unable to get version number from git tags') - exit(1) + # Forks and source snapshots may have Git metadata but no tags. + # Use a valid PEP 440 local version based on the revision instead. + cmd = 'git rev-parse --short HEAD'.split() + revision = check_output(cmd, cwd=d or '.', + stderr=PIPE).decode().strip() + version = '0+git.{0}'.format(revision) # PEP 386 compatibility if '-' in version: From a78bfe2949acf32614ced806da9b4a206e2a5c00 Mon Sep 17 00:00:00 2001 From: Maksym Sobolyev Date: Tue, 11 Aug 2026 15:18:05 -0700 Subject: [PATCH 7/7] Fix literal identity comparisons --- riak/client/__init__.py | 4 ++-- riak/datatypes/counter.py | 2 +- riak/mapreduce.py | 2 +- riak/transports/tcp/stream.py | 6 +++--- riak/transports/transport.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/riak/client/__init__.py b/riak/client/__init__.py index 7015b48f..06ca6137 100644 --- a/riak/client/__init__.py +++ b/riak/client/__init__.py @@ -341,7 +341,7 @@ def _stop_multi_pools(self): def _create_node(self, n): if isinstance(n, RiakNode): return n - elif isinstance(n, tuple) and len(n) is 3: + elif isinstance(n, tuple) and len(n) == 3: host, http_port, pb_port = n return RiakNode(host=host, http_port=http_port, @@ -382,7 +382,7 @@ def _error_rate(node): good = [n for n in nodes if _error_rate(n) < 0.1] - if len(good) is 0: + if len(good) == 0: # Fall back to a minimally broken node return min(nodes, key=_error_rate) else: diff --git a/riak/datatypes/counter.py b/riak/datatypes/counter.py index d8c8fd24..b57ac9d0 100644 --- a/riak/datatypes/counter.py +++ b/riak/datatypes/counter.py @@ -39,7 +39,7 @@ def modified(self): """ Whether this counter has staged increments. """ - return self._increment is not 0 + return self._increment != 0 def to_op(self): """ diff --git a/riak/mapreduce.py b/riak/mapreduce.py index 9a11d813..e4ef9453 100644 --- a/riak/mapreduce.py +++ b/riak/mapreduce.py @@ -359,7 +359,7 @@ def _normalize_query(self): num_phases = len(self._phases) # If there are no phases, return the keys as links - if num_phases is 0: + if num_phases == 0: link_results_flag = True else: link_results_flag = False diff --git a/riak/transports/tcp/stream.py b/riak/transports/tcp/stream.py index 95436825..15485e33 100644 --- a/riak/transports/tcp/stream.py +++ b/riak/transports/tcp/stream.py @@ -96,7 +96,7 @@ class PbufKeyStream(PbufStream): def next(self): response = super(PbufKeyStream, self).next() - if response.done and len(response.keys) is 0: + if response.done and len(response.keys) == 0: raise StopIteration return response.keys @@ -137,7 +137,7 @@ class PbufBucketStream(PbufStream): def next(self): response = super(PbufBucketStream, self).next() - if response.done and len(response.buckets) is 0: + if response.done and len(response.buckets) == 0: raise StopIteration return response.buckets @@ -199,7 +199,7 @@ def __init__(self, transport, codec, convert_timestamp=False): def next(self): response = super(PbufTsKeyStream, self).next() - if response.done and len(response.keys) is 0: + if response.done and len(response.keys) == 0: raise StopIteration keys = [] diff --git a/riak/transports/transport.py b/riak/transports/transport.py index 258d24e8..3af038a1 100644 --- a/riak/transports/transport.py +++ b/riak/transports/transport.py @@ -362,7 +362,7 @@ def _get_index_mapred_emu(self, bucket, index, startkey, endkey=None): return [key for resultbucket, key in result] def _construct_mapred_json(self, inputs, query, timeout=None): - if not self.phaseless_mapred() and (query is None or len(query) is 0): + if not self.phaseless_mapred() and (query is None or len(query) == 0): raise Exception( 'Phase-less MapReduce is not supported by Riak node')