Skip to content

Commit 89fb1dc

Browse files
authored
[Python] Add UnboundedSource ValidatesRunner test (#38892)
1 parent 704f7d8 commit 89fb1dc

8 files changed

Lines changed: 187 additions & 139 deletions
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"modification": 5
3+
"modification": 5,
4+
"https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test"
45
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"https://github.com/apache/beam/pull/32648": "testing addition of Flink 1.19 support",
33
"https://github.com/apache/beam/pull/34830": "testing",
4-
"trigger-2026-04-04": "portable_runner expand_sdf opt-in"
4+
"trigger-2026-04-04": "portable_runner expand_sdf opt-in",
5+
"https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test"
56
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"https://github.com/apache/beam/pull/34830": "testing",
33
"https://github.com/apache/beam/issues/35429": "testing",
4-
"trigger-2026-04-04": "portable_runner expand_sdf opt-in"
4+
"trigger-2026-04-04": "portable_runner expand_sdf opt-in",
5+
"https://github.com/apache/beam/pull/38892": "UnboundedSource portable VR test"
56
}

sdks/python/apache_beam/io/iobase_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,22 @@ def test_sdf_wrap_range_source(self):
224224
class UseSdfUnboundedSourcesTests(unittest.TestCase):
225225
"""Covers the UnboundedSource branch in
226226
``iobase.Read.expand()``. Uses ``UnboundedCountingSource`` from
227-
``unbounded_source_test`` as a finite fake source (no network).
227+
``unbounded_source`` as a finite fake source (no network).
228228
"""
229229
def test_read_end_to_end_unbounded(self):
230-
from apache_beam.io.unbounded_source_test import UnboundedCountingSource
230+
from apache_beam.io.unbounded_source import UnboundedCountingSource
231231
with beam.Pipeline() as p:
232232
out = p | beam.io.Read(UnboundedCountingSource(5))
233233
assert_that(out, equal_to([0, 1, 2, 3, 4]))
234234

235235
def test_read_unbounded_pcollection_is_unbounded(self):
236-
from apache_beam.io.unbounded_source_test import UnboundedCountingSource
236+
from apache_beam.io.unbounded_source import UnboundedCountingSource
237237
p = beam.Pipeline()
238238
out = p | beam.io.Read(UnboundedCountingSource(3))
239239
self.assertFalse(out.is_bounded)
240240

241241
def test_read_unbounded_serializes_as_expanded_composite(self):
242-
from apache_beam.io.unbounded_source_test import UnboundedCountingSource
242+
from apache_beam.io.unbounded_source import UnboundedCountingSource
243243
p = beam.Pipeline()
244244
p | 'ReadIt' >> beam.io.Read(UnboundedCountingSource(3))
245245

sdks/python/apache_beam/io/unbounded_source.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ def get_checkpoint_mark_coder(self):
9393
from typing import Iterable
9494
from typing import Optional
9595

96+
from typing_extensions import override
97+
9698
from apache_beam import coders
9799
from apache_beam.coders.coders import BooleanCoder
98100
from apache_beam.coders.coders import Coder
@@ -994,3 +996,141 @@ def expand(self, pbegin):
994996

995997
def _infer_output_coder(self, input_type=None, input_coder=None):
996998
return self._source.default_output_coder()
999+
1000+
1001+
# ------------------------------------------------------------------------------
1002+
# Internal in-memory counting source, shared by Beam's own tests and examples.
1003+
# ------------------------------------------------------------------------------
1004+
1005+
# Realistic event-time base away from the Unix epoch.
1006+
_EVENT_TIME_BASE = Timestamp(1729987200) # 2024-10-27T00:00:00Z
1007+
1008+
1009+
class _CountingCheckpointMark(CheckpointMark):
1010+
def __init__(self, last_index, finalize_log=None):
1011+
self.last_index = last_index
1012+
self._finalize_log = finalize_log
1013+
1014+
@override
1015+
def finalize_checkpoint(self):
1016+
if self._finalize_log is not None:
1017+
self._finalize_log.append(self.last_index)
1018+
1019+
def __eq__(self, other):
1020+
return (
1021+
isinstance(other, _CountingCheckpointMark) and
1022+
other.last_index == self.last_index)
1023+
1024+
def __hash__(self):
1025+
return hash(self.last_index)
1026+
1027+
def __repr__(self):
1028+
return '_CountingCheckpointMark(last_index=%r)' % (self.last_index, )
1029+
1030+
1031+
class _CountingReader(UnboundedReader):
1032+
def __init__(
1033+
self, count, start_index, finalize_log=None, modulus=1, residue=0):
1034+
self._count = count
1035+
self._next = start_index
1036+
self._modulus = modulus
1037+
self._residue = residue
1038+
self._current = None
1039+
self._exhausted = False
1040+
self._finalize_log = finalize_log
1041+
self.closed = False
1042+
1043+
def _read_next(self):
1044+
while self._next < self._count:
1045+
index = self._next
1046+
self._next += 1
1047+
if index % self._modulus == self._residue:
1048+
self._current = index
1049+
return True
1050+
self._exhausted = True
1051+
return False
1052+
1053+
@override
1054+
def start(self):
1055+
return self._read_next()
1056+
1057+
@override
1058+
def advance(self):
1059+
return self._read_next()
1060+
1061+
@override
1062+
def get_current(self):
1063+
return self._current
1064+
1065+
@override
1066+
def get_current_timestamp(self):
1067+
return _EVENT_TIME_BASE + self._current
1068+
1069+
@override
1070+
def get_watermark(self):
1071+
if self._exhausted:
1072+
return MAX_TIMESTAMP
1073+
if self._current is None:
1074+
return MIN_TIMESTAMP
1075+
return _EVENT_TIME_BASE + self._current
1076+
1077+
@override
1078+
def get_checkpoint_mark(self):
1079+
last = self._current if self._current is not None else self._next - 1
1080+
return _CountingCheckpointMark(last, finalize_log=self._finalize_log)
1081+
1082+
@override
1083+
def close(self):
1084+
self.closed = True
1085+
1086+
1087+
class UnboundedCountingSource(UnboundedSource):
1088+
"""In-memory counting source for Beam's own tests and examples.
1089+
1090+
Internal: not part of the public ``UnboundedSource`` API. Emits integers
1091+
``0..count-1`` with event time ``_EVENT_TIME_BASE + index``, self-terminates
1092+
at EOF, resumes from ``last_index + 1``, and optionally splits into
1093+
independent even/odd sub-sources.
1094+
"""
1095+
def __init__(
1096+
self,
1097+
count,
1098+
finalize_log=None,
1099+
is_splittable=False,
1100+
modulus=1,
1101+
residue=0):
1102+
self._count = count
1103+
self._finalize_log = finalize_log
1104+
self._is_splittable = is_splittable
1105+
self._modulus = modulus
1106+
self._residue = residue
1107+
self.last_reader = None
1108+
1109+
@override
1110+
def split(self, desired_num_splits, options=None):
1111+
if not self._is_splittable or desired_num_splits < 2:
1112+
return [self]
1113+
# Split into independent even/odd sub-sources (each non-splittable).
1114+
return [
1115+
UnboundedCountingSource(
1116+
self._count,
1117+
finalize_log=self._finalize_log,
1118+
modulus=2,
1119+
residue=residue) for residue in (0, 1)
1120+
]
1121+
1122+
@override
1123+
def create_reader(self, options, checkpoint_mark):
1124+
start_index = (
1125+
0 if checkpoint_mark is None else checkpoint_mark.last_index + 1)
1126+
self.last_reader = _CountingReader(
1127+
self._count,
1128+
start_index,
1129+
finalize_log=self._finalize_log,
1130+
modulus=self._modulus,
1131+
residue=self._residue)
1132+
return self.last_reader
1133+
1134+
@override
1135+
def get_checkpoint_mark_coder(self):
1136+
return coders.PickleCoder()

sdks/python/apache_beam/io/unbounded_source_test.py

Lines changed: 4 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@
2929
import apache_beam as beam
3030
from apache_beam import coders
3131
from apache_beam.io import unbounded_source as _unbounded_source_module
32+
from apache_beam.io.unbounded_source import _EVENT_TIME_BASE
3233
from apache_beam.io.unbounded_source import _NO_DATA
3334
from apache_beam.io.unbounded_source import CheckpointMark
3435
from apache_beam.io.unbounded_source import ReadFromUnboundedSource
36+
from apache_beam.io.unbounded_source import UnboundedCountingSource
3537
from apache_beam.io.unbounded_source import UnboundedReader
3638
from apache_beam.io.unbounded_source import UnboundedSource
39+
from apache_beam.io.unbounded_source import _CountingCheckpointMark
40+
from apache_beam.io.unbounded_source import _CountingReader
3741
from apache_beam.io.unbounded_source import _FinalizeCheckpointOnce
3842
from apache_beam.io.unbounded_source import _ReaderCache
3943
from apache_beam.io.unbounded_source import _ReadFromUnboundedSourceDoFn
@@ -55,138 +59,6 @@
5559

5660
# pylint: disable=expression-not-assigned
5761

58-
# Realistic event-time base away from the Unix epoch.
59-
_EVENT_TIME_BASE = Timestamp(1729987200) # 2024-10-27T00:00:00Z
60-
61-
# ------------------------------------------------------------------------------
62-
# In-memory demo source emitting integers 0..count-1 with event time
63-
# ``_EVENT_TIME_BASE + index``. It self-terminates at EOF, resumes from
64-
# ``last_index + 1``, and splits into even/odd sub-sources when configured.
65-
# ------------------------------------------------------------------------------
66-
67-
68-
class _CountingCheckpointMark(CheckpointMark):
69-
def __init__(self, last_index, finalize_log=None):
70-
self.last_index = last_index
71-
self._finalize_log = finalize_log
72-
73-
@override
74-
def finalize_checkpoint(self):
75-
if self._finalize_log is not None:
76-
self._finalize_log.append(self.last_index)
77-
78-
def __eq__(self, other):
79-
return (
80-
isinstance(other, _CountingCheckpointMark) and
81-
other.last_index == self.last_index)
82-
83-
def __hash__(self):
84-
return hash(self.last_index)
85-
86-
def __repr__(self):
87-
return '_CountingCheckpointMark(last_index=%r)' % (self.last_index, )
88-
89-
90-
class _CountingReader(UnboundedReader):
91-
def __init__(
92-
self, count, start_index, finalize_log=None, modulus=1, residue=0):
93-
self._count = count
94-
self._next = start_index
95-
self._modulus = modulus
96-
self._residue = residue
97-
self._current = None
98-
self._exhausted = False
99-
self._finalize_log = finalize_log
100-
self.closed = False
101-
102-
def _read_next(self):
103-
while self._next < self._count:
104-
index = self._next
105-
self._next += 1
106-
if index % self._modulus == self._residue:
107-
self._current = index
108-
return True
109-
self._exhausted = True
110-
return False
111-
112-
@override
113-
def start(self):
114-
return self._read_next()
115-
116-
@override
117-
def advance(self):
118-
return self._read_next()
119-
120-
@override
121-
def get_current(self):
122-
return self._current
123-
124-
@override
125-
def get_current_timestamp(self):
126-
return _EVENT_TIME_BASE + self._current
127-
128-
@override
129-
def get_watermark(self):
130-
if self._exhausted:
131-
return MAX_TIMESTAMP
132-
if self._current is None:
133-
return MIN_TIMESTAMP
134-
return _EVENT_TIME_BASE + self._current
135-
136-
@override
137-
def get_checkpoint_mark(self):
138-
last = self._current if self._current is not None else self._next - 1
139-
return _CountingCheckpointMark(last, finalize_log=self._finalize_log)
140-
141-
@override
142-
def close(self):
143-
self.closed = True
144-
145-
146-
class UnboundedCountingSource(UnboundedSource):
147-
def __init__(
148-
self,
149-
count,
150-
finalize_log=None,
151-
is_splittable=False,
152-
modulus=1,
153-
residue=0):
154-
self._count = count
155-
self._finalize_log = finalize_log
156-
self._is_splittable = is_splittable
157-
self._modulus = modulus
158-
self._residue = residue
159-
self.last_reader = None
160-
161-
@override
162-
def split(self, desired_num_splits, options=None):
163-
if not self._is_splittable or desired_num_splits < 2:
164-
return [self]
165-
# Split into independent even/odd sub-sources (each non-splittable).
166-
return [
167-
UnboundedCountingSource(
168-
self._count,
169-
finalize_log=self._finalize_log,
170-
modulus=2,
171-
residue=residue) for residue in (0, 1)
172-
]
173-
174-
@override
175-
def create_reader(self, options, checkpoint_mark):
176-
start_index = (
177-
0 if checkpoint_mark is None else checkpoint_mark.last_index + 1)
178-
self.last_reader = _CountingReader(
179-
self._count,
180-
start_index,
181-
finalize_log=self._finalize_log,
182-
modulus=self._modulus,
183-
residue=self._residue)
184-
return self.last_reader
185-
186-
@override
187-
def get_checkpoint_mark_coder(self):
188-
return coders.PickleCoder()
189-
19062

19163
class _StringCountingReader(_CountingReader):
19264
@override

0 commit comments

Comments
 (0)