|
42 | 42 | import cloudpickle
|
43 | 43 | from cloudpickle.cloudpickle import _is_dynamic
|
44 | 44 | from cloudpickle.cloudpickle import _make_empty_cell, cell_set
|
45 |
| -from cloudpickle.cloudpickle import _extract_class_dict |
| 45 | +from cloudpickle.cloudpickle import _extract_class_dict, _whichmodule |
46 | 46 |
|
47 | 47 | from .testutils import subprocess_pickle_echo
|
48 | 48 | from .testutils import assert_run_python_script
|
@@ -1048,35 +1048,94 @@ def __init__(self, x):
|
1048 | 1048 |
|
1049 | 1049 | self.assertEqual(set(weakset), {depickled1, depickled2})
|
1050 | 1050 |
|
1051 |
| - def test_faulty_module(self): |
1052 |
| - for module_name in ['_missing_module', None]: |
1053 |
| - class FaultyModule(object): |
1054 |
| - def __getattr__(self, name): |
1055 |
| - # This throws an exception while looking up within |
1056 |
| - # pickle.whichmodule or getattr(module, name, None) |
1057 |
| - raise Exception() |
| 1051 | + def test_non_module_object_passing_whichmodule_test(self): |
| 1052 | + # https://github.com/cloudpipe/cloudpickle/pull/326: cloudpickle should |
| 1053 | + # not try to instrospect non-modules object when trying to discover the |
| 1054 | + # module of a function/class. This happenened because codecov injects |
| 1055 | + # tuples (and not modules) into sys.modules, but type-checks were not |
| 1056 | + # carried out on the entries of sys.modules, causing cloupdickle to |
| 1057 | + # then error in unexpected ways |
| 1058 | + def func(x): |
| 1059 | + return x ** 2 |
| 1060 | + |
| 1061 | + # Trigger a loop during the execution of whichmodule(func) by |
| 1062 | + # explicitly setting the function's module to None |
| 1063 | + func.__module__ = None |
| 1064 | + |
| 1065 | + class NonModuleObject(object): |
| 1066 | + def __getattr__(self, name): |
| 1067 | + # We whitelist func so that a _whichmodule(func, None) call returns |
| 1068 | + # the NonModuleObject instance if a type check on the entries |
| 1069 | + # of sys.modules is not carried out, but manipulating this |
| 1070 | + # instance thinking it really is a module later on in the |
| 1071 | + # pickling process of func errors out |
| 1072 | + if name == 'func': |
| 1073 | + return func |
| 1074 | + else: |
| 1075 | + raise AttributeError |
| 1076 | + |
| 1077 | + non_module_object = NonModuleObject() |
| 1078 | + |
| 1079 | + assert func(2) == 4 |
| 1080 | + assert func is non_module_object.func |
| 1081 | + |
| 1082 | + # Any manipulation of non_module_object relying on attribute access |
| 1083 | + # will raise an Exception |
| 1084 | + with pytest.raises(AttributeError): |
| 1085 | + _is_dynamic(non_module_object) |
| 1086 | + |
| 1087 | + try: |
| 1088 | + sys.modules['NonModuleObject'] = non_module_object |
1058 | 1089 |
|
1059 |
| - class Foo(object): |
1060 |
| - __module__ = module_name |
| 1090 | + func_module_name = _whichmodule(func, None) |
| 1091 | + assert func_module_name != 'NonModuleObject' |
| 1092 | + assert func_module_name is None |
1061 | 1093 |
|
1062 |
| - def foo(self): |
| 1094 | + depickled_func = pickle_depickle(func, protocol=self.protocol) |
| 1095 | + assert depickled_func(2) == 4 |
| 1096 | + |
| 1097 | + finally: |
| 1098 | + sys.modules.pop('NonModuleObject') |
| 1099 | + |
| 1100 | + def test_unrelated_faulty_module(self): |
| 1101 | + # Check that pickling a dynamically defined function or class does not |
| 1102 | + # fail when introspecting the currently loaded modules in sys.modules |
| 1103 | + # as long as those faulty modules are unrelated to the class or |
| 1104 | + # function we are currently pickling. |
| 1105 | + for base_class in (object, types.ModuleType): |
| 1106 | + for module_name in ['_missing_module', None]: |
| 1107 | + class FaultyModule(base_class): |
| 1108 | + def __getattr__(self, name): |
| 1109 | + # This throws an exception while looking up within |
| 1110 | + # pickle.whichmodule or getattr(module, name, None) |
| 1111 | + raise Exception() |
| 1112 | + |
| 1113 | + class Foo(object): |
| 1114 | + __module__ = module_name |
| 1115 | + |
| 1116 | + def foo(self): |
| 1117 | + return "it works!" |
| 1118 | + |
| 1119 | + def foo(): |
1063 | 1120 | return "it works!"
|
1064 | 1121 |
|
1065 |
| - def foo(): |
1066 |
| - return "it works!" |
| 1122 | + foo.__module__ = module_name |
1067 | 1123 |
|
1068 |
| - foo.__module__ = module_name |
| 1124 | + if base_class is types.ModuleType: # noqa |
| 1125 | + faulty_module = FaultyModule('_faulty_module') |
| 1126 | + else: |
| 1127 | + faulty_module = FaultyModule() |
| 1128 | + sys.modules["_faulty_module"] = faulty_module |
1069 | 1129 |
|
1070 |
| - sys.modules["_faulty_module"] = FaultyModule() |
1071 |
| - try: |
1072 |
| - # Test whichmodule in save_global. |
1073 |
| - self.assertEqual(pickle_depickle(Foo()).foo(), "it works!") |
| 1130 | + try: |
| 1131 | + # Test whichmodule in save_global. |
| 1132 | + self.assertEqual(pickle_depickle(Foo()).foo(), "it works!") |
1074 | 1133 |
|
1075 |
| - # Test whichmodule in save_function. |
1076 |
| - cloned = pickle_depickle(foo, protocol=self.protocol) |
1077 |
| - self.assertEqual(cloned(), "it works!") |
1078 |
| - finally: |
1079 |
| - sys.modules.pop("_faulty_module", None) |
| 1134 | + # Test whichmodule in save_function. |
| 1135 | + cloned = pickle_depickle(foo, protocol=self.protocol) |
| 1136 | + self.assertEqual(cloned(), "it works!") |
| 1137 | + finally: |
| 1138 | + sys.modules.pop("_faulty_module", None) |
1080 | 1139 |
|
1081 | 1140 | def test_dynamic_pytest_module(self):
|
1082 | 1141 | # Test case for pull request https://github.com/cloudpipe/cloudpickle/pull/116
|
|
0 commit comments