|
9 | 9 | import itertools |
10 | 10 | import platform |
11 | 11 | import textwrap |
| 12 | +import base64 |
| 13 | +import subprocess |
12 | 14 |
|
13 | 15 | try: |
14 | 16 | # try importing numpy and scipy. These are not hard dependencies and |
@@ -360,6 +362,92 @@ def f(): |
360 | 362 | self.assertTrue(f2 is f3) |
361 | 363 | self.assertEqual(f2(), res) |
362 | 364 |
|
| 365 | + def test_submodule(self): |
| 366 | + # Function that refers (by attribute) to a sub-module of a package. |
| 367 | + |
| 368 | + # Choose any module NOT imported by __init__ of its parent package |
| 369 | + # examples in standard library include: |
| 370 | + # - http.cookies, unittest.mock, curses.textpad, xml.etree.ElementTree |
| 371 | + |
| 372 | + global xml # imitate performing this import at top of file |
| 373 | + import xml.etree.ElementTree |
| 374 | + def example(): |
| 375 | + x = xml.etree.ElementTree.Comment # potential AttributeError |
| 376 | + |
| 377 | + s = cloudpickle.dumps(example) |
| 378 | + |
| 379 | + # refresh the environment, i.e., unimport the dependency |
| 380 | + del xml |
| 381 | + for item in list(sys.modules): |
| 382 | + if item.split('.')[0] == 'xml': |
| 383 | + del sys.modules[item] |
| 384 | + |
| 385 | + # deserialise |
| 386 | + f = pickle.loads(s) |
| 387 | + f() # perform test for error |
| 388 | + |
| 389 | + def test_submodule_closure(self): |
| 390 | + # Same as test_submodule except the package is not a global |
| 391 | + def scope(): |
| 392 | + import xml.etree.ElementTree |
| 393 | + def example(): |
| 394 | + x = xml.etree.ElementTree.Comment # potential AttributeError |
| 395 | + return example |
| 396 | + example = scope() |
| 397 | + |
| 398 | + s = cloudpickle.dumps(example) |
| 399 | + |
| 400 | + # refresh the environment (unimport dependency) |
| 401 | + for item in list(sys.modules): |
| 402 | + if item.split('.')[0] == 'xml': |
| 403 | + del sys.modules[item] |
| 404 | + |
| 405 | + f = cloudpickle.loads(s) |
| 406 | + f() # test |
| 407 | + |
| 408 | + def test_multiprocess(self): |
| 409 | + # running a function pickled by another process (a la dask.distributed) |
| 410 | + def scope(): |
| 411 | + import curses.textpad |
| 412 | + def example(): |
| 413 | + x = xml.etree.ElementTree.Comment |
| 414 | + x = curses.textpad.Textbox |
| 415 | + return example |
| 416 | + global xml |
| 417 | + import xml.etree.ElementTree |
| 418 | + example = scope() |
| 419 | + |
| 420 | + s = cloudpickle.dumps(example) |
| 421 | + |
| 422 | + # choose "subprocess" rather than "multiprocessing" because the latter |
| 423 | + # library uses fork to preserve the parent environment. |
| 424 | + command = ("import pickle, base64; " |
| 425 | + "pickle.loads(base64.b32decode('" + |
| 426 | + base64.b32encode(s).decode('ascii') + |
| 427 | + "'))()") |
| 428 | + assert not subprocess.call([sys.executable, '-c', command]) |
| 429 | + |
| 430 | + def test_import(self): |
| 431 | + # like test_multiprocess except subpackage modules referenced directly |
| 432 | + # (unlike test_submodule) |
| 433 | + global etree |
| 434 | + def scope(): |
| 435 | + import curses.textpad as foobar |
| 436 | + def example(): |
| 437 | + x = etree.Comment |
| 438 | + x = foobar.Textbox |
| 439 | + return example |
| 440 | + example = scope() |
| 441 | + import xml.etree.ElementTree as etree |
| 442 | + |
| 443 | + s = cloudpickle.dumps(example) |
| 444 | + |
| 445 | + command = ("import pickle, base64; " |
| 446 | + "pickle.loads(base64.b32decode('" + |
| 447 | + base64.b32encode(s).decode('ascii') + |
| 448 | + "'))()") |
| 449 | + assert not subprocess.call([sys.executable, '-c', command]) |
| 450 | + |
363 | 451 |
|
364 | 452 | if __name__ == '__main__': |
365 | 453 | unittest.main() |
0 commit comments