|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +# Author: Leonardo Gama (@leogama) |
| 4 | +# Copyright (c) 2022 The Uncertainty Quantification Foundation. |
| 5 | +# License: 3-clause BSD. The full license text is available at: |
| 6 | +# - https://github.com/uqfoundation/dill/blob/master/LICENSE |
| 7 | + |
| 8 | +from __future__ import print_function |
| 9 | +import atexit, dill, os, sys, __main__ |
| 10 | + |
| 11 | +session_file = os.path.join(os.path.dirname(__file__), 'session-byref-%s.pkl') |
| 12 | + |
| 13 | +def test_modules(main, byref): |
| 14 | + main_dict = main.__dict__ |
| 15 | + |
| 16 | + try: |
| 17 | + for obj in ('json', 'url', 'local_mod', 'sax', 'dom'): |
| 18 | + assert main_dict[obj].__name__ in sys.modules |
| 19 | + |
| 20 | + for obj in ('Calendar', 'isleap'): |
| 21 | + assert main_dict[obj] is sys.modules['calendar'].__dict__[obj] |
| 22 | + assert main.day_name.__module__ == 'calendar' |
| 23 | + if byref: |
| 24 | + assert main.day_name is sys.modules['calendar'].__dict__['day_name'] |
| 25 | + |
| 26 | + assert main.complex_log is sys.modules['cmath'].__dict__['log'] |
| 27 | + |
| 28 | + except AssertionError: |
| 29 | + import traceback |
| 30 | + error_line = traceback.format_exc().splitlines()[-2].replace('[obj]', '['+repr(obj)+']') |
| 31 | + print("Error while testing (byref=%s):" % byref, error_line, sep="\n", file=sys.stderr) |
| 32 | + raise |
| 33 | + |
| 34 | + |
| 35 | +# Test session loading in a fresh interpreter session. |
| 36 | +if __name__ == '__main__' and len(sys.argv) >= 3 and sys.argv[1] == '--child': |
| 37 | + byref = sys.argv[2] == 'True' |
| 38 | + dill.load_session(session_file % byref) |
| 39 | + test_modules(__main__, byref) |
| 40 | + sys.exit() |
| 41 | + |
| 42 | +del test_modules |
| 43 | + |
| 44 | + |
| 45 | +def _clean_up_cache(module): |
| 46 | + cached = module.__file__.split('.', 1)[0] + '.pyc' |
| 47 | + cached = module.__cached__ if hasattr(module, '__cached__') else cached |
| 48 | + pycache = os.path.join(os.path.dirname(module.__file__), '__pycache__') |
| 49 | + for remove, file in [(os.remove, cached), (os.removedirs, pycache)]: |
| 50 | + try: |
| 51 | + remove(file) |
| 52 | + except OSError: |
| 53 | + pass |
| 54 | + |
| 55 | + |
| 56 | +# To clean up namespace before loading the session. |
| 57 | +original_modules = set(sys.modules.keys()) - \ |
| 58 | + set(['json', 'urllib', 'xml.sax', 'xml.dom.minidom', 'calendar', 'cmath']) |
| 59 | +original_objects = set(__main__.__dict__.keys()) |
| 60 | +original_objects.add('original_objects') |
| 61 | + |
| 62 | + |
| 63 | +# Create various kinds of objects to test different internal logics. |
| 64 | + |
| 65 | +## Modules. |
| 66 | +import json # top-level module |
| 67 | +import urllib as url # top-level module under alias |
| 68 | +from xml import sax # submodule |
| 69 | +import xml.dom.minidom as dom # submodule under alias |
| 70 | +import test_dictviews as local_mod # non-builtin top-level module |
| 71 | +atexit.register(_clean_up_cache, local_mod) |
| 72 | + |
| 73 | +## Imported objects. |
| 74 | +from calendar import Calendar, isleap, day_name # class, function, other object |
| 75 | +from cmath import log as complex_log # imported with alias |
| 76 | + |
| 77 | +## Local objects. |
| 78 | +x = 17 |
| 79 | +empty = None |
| 80 | +names = ['Alice', 'Bob', 'Carol'] |
| 81 | +def squared(x): return x**2 |
| 82 | +cubed = lambda x: x**3 |
| 83 | +class Person: |
| 84 | + def __init__(self, name, age): |
| 85 | + self.name = name |
| 86 | + self.age = age |
| 87 | +person = Person(names[0], x) |
| 88 | +class CalendarSubclass(Calendar): |
| 89 | + def weekdays(self): |
| 90 | + return [day_name[i] for i in self.iterweekdays()] |
| 91 | +cal = CalendarSubclass() |
| 92 | +selfref = __main__ |
| 93 | + |
| 94 | + |
| 95 | +def test_objects(main, copy_dict, byref): |
| 96 | + main_dict = main.__dict__ |
| 97 | + |
| 98 | + try: |
| 99 | + for obj in ('json', 'url', 'local_mod', 'sax', 'dom'): |
| 100 | + assert main_dict[obj].__name__ == copy_dict[obj].__name__ |
| 101 | + |
| 102 | + #FIXME: In the second test call, 'calendar' is not included in |
| 103 | + # sys.modules, independent of the value of byref. Tried to run garbage |
| 104 | + # collection before with no luck. This block fails even with |
| 105 | + # "import calendar" before it. Needed to restore the original modules |
| 106 | + # with the 'copy_modules' object. (Moved to "test_session_{1,2}.py".) |
| 107 | + |
| 108 | + #for obj in ('Calendar', 'isleap'): |
| 109 | + # assert main_dict[obj] is sys.modules['calendar'].__dict__[obj] |
| 110 | + #assert main_dict['day_name'].__module__ == 'calendar' |
| 111 | + #if byref: |
| 112 | + # assert main_dict['day_name'] is sys.modules['calendar'].__dict__['day_name'] |
| 113 | + |
| 114 | + for obj in ('x', 'empty', 'names'): |
| 115 | + assert main_dict[obj] == copy_dict[obj] |
| 116 | + |
| 117 | + globs = '__globals__' if dill._dill.PY3 else 'func_globals' |
| 118 | + for obj in ['squared', 'cubed']: |
| 119 | + assert getattr(main_dict[obj], globs) is main_dict |
| 120 | + assert main_dict[obj](3) == copy_dict[obj](3) |
| 121 | + |
| 122 | + assert main.Person.__module__ == main.__name__ |
| 123 | + assert isinstance(main.person, main.Person) |
| 124 | + assert main.person.age == copy_dict['person'].age |
| 125 | + |
| 126 | + assert issubclass(main.CalendarSubclass, main.Calendar) |
| 127 | + assert isinstance(main.cal, main.CalendarSubclass) |
| 128 | + assert main.cal.weekdays() == copy_dict['cal'].weekdays() |
| 129 | + |
| 130 | + assert main.selfref is main |
| 131 | + |
| 132 | + except AssertionError: |
| 133 | + import traceback |
| 134 | + error_line = traceback.format_exc().splitlines()[-2].replace('[obj]', '['+repr(obj)+']') |
| 135 | + print("Error while testing (byref=%s):" % byref, error_line, sep="\n", file=sys.stderr) |
| 136 | + raise |
| 137 | + |
| 138 | + |
| 139 | +if __name__ == '__main__': |
| 140 | + |
| 141 | + # Test dump_session() and load_session(). |
| 142 | + for byref in (False, True): |
| 143 | + if byref: |
| 144 | + # Test unpickleable imported object in main. |
| 145 | + from sys import flags |
| 146 | + |
| 147 | + #print(sorted(set(sys.modules.keys()) - original_modules)) |
| 148 | + dill._test_file = dill._dill.StringIO() |
| 149 | + try: |
| 150 | + # For the subprocess. |
| 151 | + dill.dump_session(session_file % byref, byref=byref) |
| 152 | + |
| 153 | + dill.dump_session(dill._test_file, byref=byref) |
| 154 | + dump = dill._test_file.getvalue() |
| 155 | + dill._test_file.close() |
| 156 | + |
| 157 | + import __main__ |
| 158 | + copy_dict = __main__.__dict__.copy() |
| 159 | + copy_modules = sys.modules.copy() |
| 160 | + del copy_dict['dump'] |
| 161 | + del copy_dict['__main__'] |
| 162 | + for name in copy_dict.keys(): |
| 163 | + if name not in original_objects: |
| 164 | + del __main__.__dict__[name] |
| 165 | + for module in list(sys.modules.keys()): |
| 166 | + if module not in original_modules: |
| 167 | + del sys.modules[module] |
| 168 | + |
| 169 | + dill._test_file = dill._dill.StringIO(dump) |
| 170 | + dill.load_session(dill._test_file) |
| 171 | + #print(sorted(set(sys.modules.keys()) - original_modules)) |
| 172 | + |
| 173 | + # Test session loading in a new session. |
| 174 | + from dill.tests.__main__ import python, shell, sp |
| 175 | + error = sp.call([python, __file__, '--child', str(byref)], shell=shell) |
| 176 | + if error: sys.exit(error) |
| 177 | + del python, shell, sp |
| 178 | + |
| 179 | + finally: |
| 180 | + dill._test_file.close() |
| 181 | + try: |
| 182 | + os.remove(session_file % byref) |
| 183 | + except OSError: |
| 184 | + pass |
| 185 | + |
| 186 | + test_objects(__main__, copy_dict, byref) |
| 187 | + __main__.__dict__.update(copy_dict) |
| 188 | + sys.modules.update(copy_modules) |
| 189 | + del __main__, copy_dict, copy_modules, dump |
| 190 | + |
| 191 | + |
| 192 | + # This is for code coverage, tests the use case of dump_session(byref=True) |
| 193 | + # without imported objects in the namespace. It's a contrived example because |
| 194 | + # even dill can't be in it. |
| 195 | + from types import ModuleType |
| 196 | + modname = '__test_main__' |
| 197 | + main = ModuleType(modname) |
| 198 | + main.x = 42 |
| 199 | + |
| 200 | + _main = dill._dill._stash_modules(main) |
| 201 | + if _main is not main: |
| 202 | + print("There are objects to save by referenece that shouldn't be:", |
| 203 | + _main.__dill_imported, _main.__dill_imported_as, _main.__dill_imported_top_level, |
| 204 | + file=sys.stderr) |
| 205 | + |
| 206 | + test_file = dill._dill.StringIO() |
| 207 | + try: |
| 208 | + dill.dump_session(test_file, main=main, byref=True) |
| 209 | + dump = test_file.getvalue() |
| 210 | + test_file.close() |
| 211 | + |
| 212 | + sys.modules[modname] = ModuleType(modname) # empty |
| 213 | + # This should work after fixing https://github.com/uqfoundation/dill/issues/462 |
| 214 | + test_file = dill._dill.StringIO(dump) |
| 215 | + dill.load_session(test_file) |
| 216 | + finally: |
| 217 | + test_file.close() |
| 218 | + |
| 219 | + assert x == 42 |
| 220 | + |
| 221 | + |
| 222 | + # Dump session for module that is not __main__: |
| 223 | + import test_classdef as module |
| 224 | + atexit.register(_clean_up_cache, module) |
| 225 | + module.selfref = module |
| 226 | + dict_objects = [obj for obj in module.__dict__.keys() if not obj.startswith('__')] |
| 227 | + |
| 228 | + test_file = dill._dill.StringIO() |
| 229 | + try: |
| 230 | + dill.dump_session(test_file, main=module) |
| 231 | + dump = test_file.getvalue() |
| 232 | + test_file.close() |
| 233 | + |
| 234 | + for obj in dict_objects: |
| 235 | + del module.__dict__[obj] |
| 236 | + |
| 237 | + test_file = dill._dill.StringIO(dump) |
| 238 | + dill.load_session(test_file, main=module) |
| 239 | + finally: |
| 240 | + test_file.close() |
| 241 | + |
| 242 | + assert all(obj in module.__dict__ for obj in dict_objects) |
| 243 | + assert module.selfref is module |
0 commit comments