Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-kafka-python for
openSUSE:Factory checked in at 2026-08-22 21:35:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-kafka-python (Old)
and /work/SRC/openSUSE:Factory/.python-kafka-python.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-kafka-python"
Sat Aug 22 21:35:41 2026 rev:17 rq:1373030 version:3.0.11
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-kafka-python/python-kafka-python.changes
2026-07-15 17:01:39.815234631 +0200
+++
/work/SRC/openSUSE:Factory/.python-kafka-python.new.1258/python-kafka-python.changes
2026-08-22 21:37:49.582829241 +0200
@@ -1,0 +2,11 @@
+Sat Aug 22 09:19:56 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.0.11:
+ * Fix JsonSerializer returning None and raising TypeError on
+ deserialize
+ * Admin: Fix `create_partitions` sending empty assignments
+ array instead of null
+ * Consumer: fix stuck partitions retained after rebalance with
+ listener
+
+-------------------------------------------------------------------
Old:
----
python-kafka-python-3.0.8.tar.gz
New:
----
python-kafka-python-3.0.11.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-kafka-python.spec ++++++
--- /var/tmp/diff_new_pack.M3RPQz/_old 2026-08-22 21:37:50.187850875 +0200
+++ /var/tmp/diff_new_pack.M3RPQz/_new 2026-08-22 21:37:50.189850946 +0200
@@ -23,7 +23,7 @@
%endif
Name: python-kafka-python
-Version: 3.0.8
+Version: 3.0.11
Release: 0
Summary: Pure Python client for Apache Kafka
License: Apache-2.0
++++++ python-kafka-python-3.0.8.tar.gz -> python-kafka-python-3.0.11.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/CHANGES.md
new/kafka-python-3.0.11/CHANGES.md
--- old/kafka-python-3.0.8/CHANGES.md 2026-07-09 20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/CHANGES.md 2026-08-16 18:29:39.000000000 +0200
@@ -1,3 +1,18 @@
+# 3.0.11 (Aug 16, 2026)
+
+### Fixes
+* Fix JsonSerializer returning None and raising TypeError on deserialize
(#3150)
+
+# 3.0.10 (Aug 4, 2026)
+
+### Fixes
+* Admin: Fix `create_partitions` sending empty assignments array instead of
null (#3148)
+
+# 3.0.9 (Jul 21, 2026)
+
+### Fixes
+* Consumer: fix stuck partitions retained after rebalance with listener (#3132)
+
# 3.0.8 (Jul 9, 2026)
### Fixes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/docs/changelog.rst
new/kafka-python-3.0.11/docs/changelog.rst
--- old/kafka-python-3.0.8/docs/changelog.rst 2026-07-09 20:43:02.000000000
+0200
+++ new/kafka-python-3.0.11/docs/changelog.rst 2026-08-16 18:29:39.000000000
+0200
@@ -1,6 +1,30 @@
Changelog
=========
+3.0.11 (Aug 16, 2026)
+#####################
+
+Fixes
+-----
+* Fix JsonSerializer returning None and raising TypeError on deserialize
(#3150)
+
+
+3.0.10 (Aug 4, 2026)
+####################
+
+Fixes
+-----
+* Admin: Fix `create_partitions` sending empty assignments array instead of
null (#3148)
+
+
+3.0.9 (Jul 21, 2026)
+####################
+
+Fixes
+-----
+* Consumer: fix stuck partitions retained after rebalance with listener (#3132)
+
+
3.0.8 (Jul 9, 2026)
###################
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/kafka/admin/_partitions.py
new/kafka-python-3.0.11/kafka/admin/_partitions.py
--- old/kafka-python-3.0.8/kafka/admin/_partitions.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/kafka/admin/_partitions.py 2026-08-16
18:29:39.000000000 +0200
@@ -54,12 +54,17 @@
assignments=[_Assignment(broker_ids=broker_ids)
for broker_ids in count['assignments']]))
else:
+ # Deprecated dict[topic, NewPartitions]
+ if count.new_assignments is not None:
+ new_assignments = [_Assignment(broker_ids=broker_ids)
+ for broker_ids in count.new_assignments]
+ else:
+ new_assignments = None
topics.append(
_Topic(
name=topic,
count=count.total_count,
- assignments=[_Assignment(broker_ids=broker_ids)
- for broker_ids in count.new_assignments]))
+ assignments=new_assignments))
return topics
def create_partitions(self, topic_partitions, timeout_ms=None,
validate_only=False, raise_errors=True):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/kafka/admin/_topics.py
new/kafka-python-3.0.11/kafka/admin/_topics.py
--- old/kafka-python-3.0.8/kafka/admin/_topics.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/kafka/admin/_topics.py 2026-08-16
18:29:39.000000000 +0200
@@ -12,7 +12,7 @@
import kafka.errors as Errors
from kafka.errors import IncompatibleBrokerVersion
-from kafka.protocol.admin import CreateTopicsRequest, DeleteTopicsRequest,
CreatePartitionsRequest
+from kafka.protocol.admin import CreateTopicsRequest, DeleteTopicsRequest
from ._configs import ConfigResourceType
if TYPE_CHECKING:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/kafka-python-3.0.8/kafka/consumer/subscription_state.py
new/kafka-python-3.0.11/kafka/consumer/subscription_state.py
--- old/kafka-python-3.0.8/kafka/consumer/subscription_state.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/kafka/consumer/subscription_state.py
2026-08-16 18:29:39.000000000 +0200
@@ -290,6 +290,9 @@
for tp in list(self.assignment.keys()):
if tp not in new_set:
del self.assignment[tp]
+ else:
+ # Retained partitions are no longer pending_revocation
+ self.assignment[tp].mark_pending_revocation(False)
# Add new partitions; kept partitions retain their existing
# TopicPartitionState (positions, paused flag, KIP-392 cache,
# etc.).
@@ -651,12 +654,12 @@
def resume(self):
self.paused = False
- def mark_pending_revocation(self):
+ def mark_pending_revocation(self, val=True):
"""KIP-429: gate fetches while an on_partitions_revoked /
on_partitions_lost listener is in progress for this partition.
Single-shot: the surrounding ``assign_from_subscribed`` drops
the state object once the listener returns."""
- self._pending_revocation = True
+ self._pending_revocation = val
def is_fetchable(self):
return (not self.paused
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/kafka/protocol/admin/topics.py
new/kafka-python-3.0.11/kafka/protocol/admin/topics.py
--- old/kafka-python-3.0.8/kafka/protocol/admin/topics.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/kafka/protocol/admin/topics.py 2026-08-16
18:29:39.000000000 +0200
@@ -17,7 +17,12 @@
class DeleteTopicsResponse(ApiMessage): pass
-class CreatePartitionsRequest(ApiMessage): pass
+class CreatePartitionsRequest(ApiMessage):
+ @classmethod
+ def json_patch(cls, json):
+ json['fields'][0]['fields'][2]['default'] = 'null'
+ return json
+
class CreatePartitionsResponse(ApiMessage): pass
class AlterPartitionReassignmentsRequest(ApiMessage): pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/kafka/serializer/json.py
new/kafka-python-3.0.11/kafka/serializer/json.py
--- old/kafka-python-3.0.8/kafka/serializer/json.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/kafka/serializer/json.py 2026-08-16
18:29:39.000000000 +0200
@@ -1,9 +1,9 @@
import json
-from .abstract import Serializer, Deserializer
+from .default import DefaultSerializer
-class JsonSerializer(Serializer, Deserializer):
+class JsonSerializer(DefaultSerializer):
def serialize(self, topic, headers, data):
if data is None:
return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/kafka/version.py
new/kafka-python-3.0.11/kafka/version.py
--- old/kafka-python-3.0.8/kafka/version.py 2026-07-09 20:43:02.000000000
+0200
+++ new/kafka-python-3.0.11/kafka/version.py 2026-08-16 18:29:39.000000000
+0200
@@ -1 +1 @@
-__version__ = '3.0.8'
+__version__ = '3.0.11'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/test/admin/test_admin_topics.py
new/kafka-python-3.0.11/test/admin/test_admin_topics.py
--- old/kafka-python-3.0.8/test/admin/test_admin_topics.py 2026-07-09
20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/test/admin/test_admin_topics.py 2026-08-16
18:29:39.000000000 +0200
@@ -22,6 +22,40 @@
assert good_partitions.new_assignments == [[1, 2, 3]]
+def test_process_create_partitions_input_auto_assign_is_null():
+ # When the caller does not supply manual replica assignments, the request's
+ # `assignments` field must be None so it serialises to null (broker
+ # auto-assigns). An empty list [] serialises to a present-but-empty array,
+ # which the broker reads as "manual assignment" and rejects with
+ # InvalidReplicationAssignmentError. See issue #3146.
+ proc = KafkaAdminClient._process_create_partitions_input
+
+ # int total-count -> auto-assign
+ topics = proc({'foo': 6})
+ assert len(topics) == 1
+ assert topics[0].name == 'foo'
+ assert topics[0].count == 6
+ assert topics[0].assignments is None
+
+ # bare NewPartitions with no manual assignments -> auto-assign
+ topics = proc({'foo': NewPartitions(6)})
+ assert topics[0].assignments is None
+
+
+def test_process_create_partitions_input_manual_assignments():
+ # Explicit manual assignments must be preserved, for both the dict form
+ # and the (deprecated) NewPartitions form.
+ proc = KafkaAdminClient._process_create_partitions_input
+
+ topics = proc({'foo': {'count': 7, 'assignments': [[1, 2, 3]]}})
+ assert topics[0].count == 7
+ assert [a.broker_ids for a in topics[0].assignments] == [[1, 2, 3]]
+
+ topics = proc({'foo': NewPartitions(7, [[1, 2, 3]])})
+ assert topics[0].count == 7
+ assert [a.broker_ids for a in topics[0].assignments] == [[1, 2, 3]]
+
+
def test_new_topic():
good_topic = NewTopic('foo')
assert good_topic.name == 'foo'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/kafka-python-3.0.8/test/consumer/test_subscription_state.py
new/kafka-python-3.0.11/test/consumer/test_subscription_state.py
--- old/kafka-python-3.0.8/test/consumer/test_subscription_state.py
2026-07-09 20:43:02.000000000 +0200
+++ new/kafka-python-3.0.11/test/consumer/test_subscription_state.py
2026-08-16 18:29:39.000000000 +0200
@@ -141,6 +141,49 @@
# Still gates the assigned one.
assert s.assignment[TopicPartition('foo', 0)]._pending_revocation is
True
+ def
test_pending_revocation_cleared_for_partition_retained_across_rebalance(self):
+ """Regression test for #3131.
+
+ Under the EAGER rebalance protocol the coordinator marks *every*
+ currently-owned partition pending-revocation before JoinGroup (see
+ ConsumerCoordinator._on_join_prepare_async). When the rebalance
+ completes, ``assign_from_subscribed`` preserves the
+ ``TopicPartitionState`` for any partition the member is given back,
+ so a *retained* partition carries a stale ``_pending_revocation``
+ flag. Because the flag is only ever cleared in
+ ``TopicPartitionState.__init__``, such a partition reports
+ not-fetchable forever - the fetcher positions it (ListOffsets) but
+ never issues a FetchRequest - until a later rebalance drops it and
+ creates fresh state.
+
+ A partition retained across a rebalance must be fetchable again.
+ """
+ s = self._subscribed()
+ foo0 = TopicPartition('foo', 0)
+ foo1 = TopicPartition('foo', 1)
+ s.assign_from_subscribed([foo0, foo1])
+ for tp in (foo0, foo1):
+ s.assignment[tp].seek(
+ OffsetAndMetadata(offset=0, metadata='', leader_epoch=-1))
+ assert s.is_fetchable(foo0)
+ assert s.is_fetchable(foo1)
+
+ # EAGER pre-rebalance: mark the full current assignment for revocation
+ # (mirrors mark_pending_revocation(assigned_partitions())).
+ s.mark_pending_revocation({foo0, foo1})
+ assert not s.is_fetchable(foo0)
+ assert not s.is_fetchable(foo1)
+
+ # Rebalance completes and the member is assigned the same partitions
+ # back. Their state (and valid position) is preserved.
+ s.assign_from_subscribed([foo0, foo1])
+ assert s.assignment[foo0].has_valid_position
+ assert s.assignment[foo1].has_valid_position
+
+ # They must be fetchable again - the revocation window is over.
+ assert s.is_fetchable(foo0)
+ assert s.is_fetchable(foo1)
+
def test_preserves_preferred_read_replica_on_kept_partition(self):
s = self._subscribed()
s.assign_from_subscribed([TopicPartition('foo', 0)])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/kafka-python-3.0.8/test/test_serializer.py
new/kafka-python-3.0.11/test/test_serializer.py
--- old/kafka-python-3.0.8/test/test_serializer.py 1970-01-01
01:00:00.000000000 +0100
+++ new/kafka-python-3.0.11/test/test_serializer.py 2026-08-16
18:29:39.000000000 +0200
@@ -0,0 +1,47 @@
+# pylint: skip-file
+
+import pytest
+
+from kafka.serializer import DefaultSerializer, JsonSerializer
+
+
[email protected]('encoding', ['utf-8', 'utf-16'])
+def test_default_serializer_roundtrip(encoding):
+ ser = DefaultSerializer(encoding)
+ data = 'h\u00e9llo w\u00f6rld'
+ encoded = ser.serialize('topic', [], data)
+ assert isinstance(encoded, bytes)
+ assert ser.deserialize('topic', [], encoded) == data
+
+
[email protected]('data', [b'raw', bytearray(b'raw'),
memoryview(b'raw'), None])
+def test_default_serializer_passthrough_bytes_like(data):
+ assert DefaultSerializer().serialize('topic', [], data) == data
+
+
+def test_default_serializer_deserialize_none():
+ assert DefaultSerializer().deserialize('topic', [], None) is None
+
+
+def test_default_serializer_rejects_non_bytes_str():
+ with pytest.raises(AttributeError):
+ DefaultSerializer().serialize('topic', [], 42)
+
+
+def test_json_serializer_roundtrip():
+ ser = JsonSerializer()
+ data = {'key': 'value', 'n': 42, 'lst': [1, 2]}
+ encoded = ser.serialize('topic', [], data)
+ assert isinstance(encoded, bytes)
+ assert ser.deserialize('topic', [], encoded) == data
+
+
+def test_json_serializer_none():
+ ser = JsonSerializer()
+ assert ser.serialize('topic', [], None) is None
+ assert ser.deserialize('topic', [], None) is None
+
+
+def test_json_serializer_deserializes_plain_json():
+ ser = JsonSerializer()
+ assert ser.deserialize('topic', [], b'{"a": 1}') == {'a': 1}