Skip to content

Commit 71ab88b

Browse files
committed
Make dill optional
1 parent 32f5be6 commit 71ab88b

17 files changed

Lines changed: 189 additions & 16 deletions

File tree

plugins/beam-code-completion-plugin/.run/Run IDE with Plugin.run.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040
<DebugAllEnabled>false</DebugAllEnabled>
4141
<method v="2" />
4242
</configuration>
43-
</component>
43+
</component>

sdks/python/apache_beam/coders/coders.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@
8585
# occurs.
8686
from apache_beam.internal.dill_pickler import dill
8787
except ImportError:
88-
# We fall back to using the stock dill library in tests that don't use the
89-
# full Python SDK.
90-
import dill
88+
dill = None # type: ignore
9189

9290
__all__ = [
9391
'Coder',
@@ -900,6 +898,12 @@ def to_type_hint(self):
900898

901899
class DillCoder(_PickleCoderBase):
902900
"""Coder using dill's pickle functionality."""
901+
def __init__(self):
902+
if not dill:
903+
raise RuntimeError("This pipeline contains a DillCoder which requires " \
904+
"the dill package. Install the dill package with the dill extra " \
905+
"e.g. apache-beam[dill]")
906+
903907
def _create_impl(self):
904908
return coder_impl.CallbackCoderImpl(maybe_dill_dumps, maybe_dill_loads)
905909

sdks/python/apache_beam/coders/coders_test_common.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
except ImportError:
6060
dataclasses = None # type: ignore
6161

62+
try:
63+
import dill
64+
except ImportError:
65+
dill = None
66+
6267
MyNamedTuple = collections.namedtuple('A', ['x', 'y']) # type: ignore[name-match]
6368
AnotherNamedTuple = collections.namedtuple('AnotherNamedTuple', ['x', 'y'])
6469
MyTypedNamedTuple = NamedTuple('MyTypedNamedTuple', [('f1', int), ('f2', str)])
@@ -116,6 +121,7 @@ class UnFrozenDataClass:
116121
# These tests need to all be run in the same process due to the asserts
117122
# in tearDownClass.
118123
@pytest.mark.no_xdist
124+
@pytest.mark.uses_dill
119125
class CodersTest(unittest.TestCase):
120126

121127
# These class methods ensure that we test each defined coder in both
@@ -173,6 +179,9 @@ def tearDownClass(cls):
173179
coders.BigIntegerCoder, # tested in DecimalCoder
174180
coders.TimestampPrefixingOpaqueWindowCoder,
175181
])
182+
if not dill:
183+
standard -= set(
184+
[coders.DillCoder, coders.DeterministicFastPrimitivesCoder])
176185
cls.seen_nested -= set(
177186
[coders.ProtoCoder, coders.ProtoPlusCoder, CustomCoder])
178187
assert not standard - cls.seen, str(standard - cls.seen)
@@ -241,8 +250,13 @@ def test_memoizing_pickle_coder(self):
241250
param(compat_version="2.67.0"),
242251
])
243252
def test_deterministic_coder(self, compat_version):
253+
244254
typecoders.registry.update_compatibility_version = compat_version
245255
coder = coders.FastPrimitivesCoder()
256+
if not dill and compat_version:
257+
with self.assertRaises(RuntimeError):
258+
coder.as_deterministic_coder(step_label="step")
259+
self.skipTest('Dill not installed')
246260
deterministic_coder = coder.as_deterministic_coder(step_label="step")
247261

248262
self.check_coder(deterministic_coder, *self.test_values_deterministic)
@@ -321,6 +335,11 @@ def test_deterministic_map_coder_is_update_compatible(self, compat_version):
321335
coder = coders.MapCoder(
322336
coders.FastPrimitivesCoder(), coders.FastPrimitivesCoder())
323337

338+
if not dill and compat_version:
339+
with self.assertRaises(RuntimeError):
340+
coder.as_deterministic_coder(step_label="step")
341+
self.skipTest('Dill not installed')
342+
324343
deterministic_coder = coder.as_deterministic_coder(step_label="step")
325344

326345
assert isinstance(
@@ -331,6 +350,11 @@ def test_deterministic_map_coder_is_update_compatible(self, compat_version):
331350
self.check_coder(deterministic_coder, *values)
332351

333352
def test_dill_coder(self):
353+
if not dill:
354+
with self.assertRaises(RuntimeError):
355+
coders.DillCoder()
356+
self.skipTest('Dill not installed')
357+
334358
cell_value = (lambda x: lambda: x)(0).__closure__[0]
335359
self.check_coder(coders.DillCoder(), 'a', 1, cell_value)
336360
self.check_coder(
@@ -661,6 +685,8 @@ def test_param_windowed_value_coder(self):
661685
def test_cross_process_encoding_of_special_types_is_deterministic(
662686
self, compat_version):
663687
"""Test cross-process determinism for all special deterministic types"""
688+
if compat_version:
689+
pytest.importorskip("dill")
664690

665691
if sys.executable is None:
666692
self.skipTest('No Python interpreter found')

sdks/python/apache_beam/internal/pickler.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
"""
3030

3131
from apache_beam.internal import cloudpickle_pickler
32-
from apache_beam.internal import dill_pickler
32+
33+
try:
34+
from apache_beam.internal import dill_pickler
35+
except ImportError:
36+
dill_pickler = None
3337

3438
USE_CLOUDPICKLE = 'cloudpickle'
3539
USE_DILL = 'dill'
@@ -74,6 +78,12 @@ def load_session(file_path):
7478
def set_library(selected_library=DEFAULT_PICKLE_LIB):
7579
""" Sets pickle library that will be used. """
7680
global desired_pickle_lib
81+
82+
if selected_library == USE_DILL and not dill_pickler:
83+
if not dill_pickler:
84+
raise ImportError(
85+
"Dill is not installed. To use dill pickler, install apache-beam with"
86+
" the dill extras package e.g. apache-beam[dill].")
7787
# If switching to or from dill, update the pickler hook overrides.
7888
if (selected_library == USE_DILL) != (desired_pickle_lib == dill_pickler):
7989
dill_pickler.override_pickler_hooks(selected_library == USE_DILL)

sdks/python/apache_beam/internal/pickler_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
# pytype: skip-file
2121

22+
import pytest
2223
import random
2324
import sys
2425
import threading
@@ -34,6 +35,12 @@
3435
from apache_beam.internal.pickler import loads
3536

3637

38+
def maybe_skip_if_no_dill(pickle_library):
39+
if pickle_library == 'dill':
40+
pytest.importorskip("dill")
41+
42+
43+
@pytest.mark.uses_dill
3744
class PicklerTest(unittest.TestCase):
3845

3946
NO_MAPPINGPROXYTYPE = not hasattr(types, "MappingProxyType")
@@ -43,6 +50,7 @@ class PicklerTest(unittest.TestCase):
4350
param(pickle_lib='cloudpickle'),
4451
])
4552
def test_basics(self, pickle_lib):
53+
maybe_skip_if_no_dill(pickle_lib)
4654
pickler.set_library(pickle_lib)
4755

4856
self.assertEqual([1, 'a', ('z', )], loads(dumps([1, 'a', ('z', )])))
@@ -55,6 +63,7 @@ def test_basics(self, pickle_lib):
5563
])
5664
def test_lambda_with_globals(self, pickle_lib):
5765
"""Tests that the globals of a function are preserved."""
66+
maybe_skip_if_no_dill(pickle_lib)
5867
pickler.set_library(pickle_lib)
5968

6069
# The point of the test is that the lambda being called after unpickling
@@ -68,6 +77,7 @@ def test_lambda_with_globals(self, pickle_lib):
6877
param(pickle_lib='cloudpickle'),
6978
])
7079
def test_lambda_with_main_globals(self, pickle_lib):
80+
maybe_skip_if_no_dill(pickle_lib)
7181
pickler.set_library(pickle_lib)
7282
self.assertEqual(unittest, loads(dumps(lambda: unittest))())
7383

@@ -77,6 +87,7 @@ def test_lambda_with_main_globals(self, pickle_lib):
7787
])
7888
def test_lambda_with_closure(self, pickle_lib):
7989
"""Tests that the closure of a function is preserved."""
90+
maybe_skip_if_no_dill(pickle_lib)
8091
pickler.set_library(pickle_lib)
8192
self.assertEqual(
8293
'closure: abc',
@@ -88,6 +99,7 @@ def test_lambda_with_closure(self, pickle_lib):
8899
])
89100
def test_class(self, pickle_lib):
90101
"""Tests that a class object is pickled correctly."""
102+
maybe_skip_if_no_dill(pickle_lib)
91103
pickler.set_library(pickle_lib)
92104
self.assertEqual(['abc', 'def'],
93105
loads(dumps(module_test.Xyz))().foo('abc def'))
@@ -98,6 +110,7 @@ def test_class(self, pickle_lib):
98110
])
99111
def test_object(self, pickle_lib):
100112
"""Tests that a class instance is pickled correctly."""
113+
maybe_skip_if_no_dill(pickle_lib)
101114
pickler.set_library(pickle_lib)
102115
self.assertEqual(['abc', 'def'],
103116
loads(dumps(module_test.XYZ_OBJECT)).foo('abc def'))
@@ -108,6 +121,7 @@ def test_object(self, pickle_lib):
108121
])
109122
def test_nested_class(self, pickle_lib):
110123
"""Tests that a nested class object is pickled correctly."""
124+
maybe_skip_if_no_dill(pickle_lib)
111125
pickler.set_library(pickle_lib)
112126
self.assertEqual(
113127
'X:abc', loads(dumps(module_test.TopClass.NestedClass('abc'))).datum)
@@ -121,6 +135,7 @@ def test_nested_class(self, pickle_lib):
121135
])
122136
def test_dynamic_class(self, pickle_lib):
123137
"""Tests that a nested class object is pickled correctly."""
138+
maybe_skip_if_no_dill(pickle_lib)
124139
pickler.set_library(pickle_lib)
125140
self.assertEqual(
126141
'Z:abc', loads(dumps(module_test.create_class('abc'))).get())
@@ -130,6 +145,7 @@ def test_dynamic_class(self, pickle_lib):
130145
param(pickle_lib='cloudpickle'),
131146
])
132147
def test_generators(self, pickle_lib):
148+
maybe_skip_if_no_dill(pickle_lib)
133149
pickler.set_library(pickle_lib)
134150
with self.assertRaises(TypeError):
135151
dumps((_ for _ in range(10)))
@@ -139,6 +155,7 @@ def test_generators(self, pickle_lib):
139155
param(pickle_lib='cloudpickle'),
140156
])
141157
def test_recursive_class(self, pickle_lib):
158+
maybe_skip_if_no_dill(pickle_lib)
142159
pickler.set_library(pickle_lib)
143160
self.assertEqual(
144161
'RecursiveClass:abc',
@@ -149,6 +166,7 @@ def test_recursive_class(self, pickle_lib):
149166
param(pickle_lib='cloudpickle'),
150167
])
151168
def test_pickle_rlock(self, pickle_lib):
169+
maybe_skip_if_no_dill(pickle_lib)
152170
pickler.set_library(pickle_lib)
153171
rlock_instance = threading.RLock()
154172
rlock_type = type(rlock_instance)
@@ -160,6 +178,7 @@ def test_pickle_rlock(self, pickle_lib):
160178
param(pickle_lib='cloudpickle'),
161179
])
162180
def test_save_paths(self, pickle_lib):
181+
maybe_skip_if_no_dill(pickle_lib)
163182
pickler.set_library(pickle_lib)
164183
f = loads(dumps(lambda x: x))
165184
co_filename = f.__code__.co_filename
@@ -171,6 +190,7 @@ def test_save_paths(self, pickle_lib):
171190
param(pickle_lib='cloudpickle'),
172191
])
173192
def test_dump_and_load_mapping_proxy(self, pickle_lib):
193+
maybe_skip_if_no_dill(pickle_lib)
174194
pickler.set_library(pickle_lib)
175195
self.assertEqual(
176196
'def', loads(dumps(types.MappingProxyType({'abc': 'def'})))['abc'])
@@ -184,6 +204,7 @@ def test_dump_and_load_mapping_proxy(self, pickle_lib):
184204
param(pickle_lib='cloudpickle'),
185205
])
186206
def test_dataclass(self, pickle_lib):
207+
maybe_skip_if_no_dill(pickle_lib)
187208
exec(
188209
'''
189210
from apache_beam.internal.module_test import DataClass
@@ -195,6 +216,7 @@ def test_dataclass(self, pickle_lib):
195216
param(pickle_lib='cloudpickle'),
196217
])
197218
def test_class_states_not_changed_at_subsequent_loading(self, pickle_lib):
219+
maybe_skip_if_no_dill(pickle_lib)
198220
pickler.set_library(pickle_lib)
199221

200222
class Local:
@@ -255,6 +277,7 @@ def maybe_get_sets_with_different_iteration_orders(self):
255277
return set1, set2
256278

257279
def test_best_effort_determinism(self):
280+
maybe_skip_if_no_dill('dill')
258281
pickler.set_library('dill')
259282
set1, set2 = self.maybe_get_sets_with_different_iteration_orders()
260283
self.assertEqual(
@@ -267,6 +290,7 @@ def test_best_effort_determinism(self):
267290
self.skipTest('Set iteration orders matched. Test results inconclusive.')
268291

269292
def test_disable_best_effort_determinism(self):
293+
maybe_skip_if_no_dill('dill')
270294
pickler.set_library('dill')
271295
set1, set2 = self.maybe_get_sets_with_different_iteration_orders()
272296
# The test relies on the sets having different iteration orders for the

sdks/python/apache_beam/ml/anomaly/specifiable_test.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import dataclasses
2020
import logging
2121
import os
22+
import pytest
2223
import unittest
2324
from typing import Optional
2425

@@ -323,7 +324,10 @@ def __init__(self, arg):
323324
self.my_arg = arg * 10
324325
type(self).counter += 1
325326

326-
def test_on_pickle(self):
327+
@pytest.mark.uses_dill
328+
def test_on_dill_pickle(self):
329+
pytest.importorskip("dill")
330+
327331
FooForPickle = TestInitCallCount.FooForPickle
328332

329333
import dill
@@ -339,6 +343,9 @@ def test_on_pickle(self):
339343
self.assertEqual(FooForPickle.counter, 1)
340344
self.assertEqual(new_foo_2.__dict__, foo.__dict__)
341345

346+
def test_on_pickle(self):
347+
FooForPickle = TestInitCallCount.FooForPickle
348+
342349
# Note that pickle does not support classes/functions nested in a function.
343350
import pickle
344351
FooForPickle.counter = 0

sdks/python/apache_beam/options/pipeline_options.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,7 @@ def _add_argparse_args(cls, parser):
16191619
help=(
16201620
'Chooses which pickle library to use. Options are dill, '
16211621
'cloudpickle or default.'),
1622-
choices=['cloudpickle', 'default', 'dill'])
1622+
choices=['cloudpickle', 'default', 'dill', 'dill_unsafe'])
16231623
parser.add_argument(
16241624
'--save_main_session',
16251625
default=False,
@@ -1701,6 +1701,7 @@ def _add_argparse_args(cls, parser):
17011701
def validate(self, validator):
17021702
errors = []
17031703
errors.extend(validator.validate_container_prebuilding_options(self))
1704+
errors.extend(validator.validate_pickle_library(self))
17041705
return errors
17051706

17061707

sdks/python/apache_beam/options/pipeline_options_validator.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ class PipelineOptionsValidator(object):
119119
ERR_REPEATABLE_OPTIONS_NOT_SET_AS_LIST = (
120120
'(%s) is a string. Programmatically set PipelineOptions like (%s) '
121121
'options need to be specified as a list.')
122+
ERR_DILL_NOT_INSTALLED = (
123+
'Option pickle_library=dill requires dill==0.3.1.1. Install apache-beam '
124+
'with the dill extra e.g. apache-beam[gcp, dill]. Dill package was not '
125+
'found')
126+
ERR_UNSAFE_DILL_VERSION = (
127+
'Dill version 0.3.1.1 is required when using pickle_library=dill. Other'
128+
'versions of dill are untested with Apache Beam. To install the supported'
129+
'dill version instal apache-beam[dill] extra. To use an unsupported dill '
130+
'version, use pickle_library=dill_unsafe. %s')
122131

123132
# GCS path specific patterns.
124133
GCS_URI = '(?P<SCHEME>[^:]+)://(?P<BUCKET>[^/]+)(/(?P<OBJECT>.*))?'
@@ -196,6 +205,25 @@ def validate_gcs_path(self, view, arg_name):
196205
return self._validate_error(self.ERR_INVALID_GCS_OBJECT, arg, arg_name)
197206
return []
198207

208+
def validate_pickle_library(self, view):
209+
"""Validates a GCS path against gs://bucket/object URI format."""
210+
if view.pickle_library == 'default' or view.pickle_library == 'cloudpickle':
211+
return []
212+
213+
if view.pickle_library == 'dill_unsafe':
214+
return []
215+
216+
if view.pickle_library == 'dill':
217+
try:
218+
import dill
219+
if dill.__version__ != "0.3.1.1":
220+
return self._validate_error(
221+
self.ERR_UNSAFE_DILL_VERSION,
222+
f"Dill version found {dill.__version__}")
223+
except ImportError:
224+
return self._validate_error(self.ERR_DILL_NOT_INSTALLED)
225+
return []
226+
199227
def validate_cloud_options(self, view):
200228
"""Validates job_name and project arguments."""
201229
errors = []

0 commit comments

Comments
 (0)