#!/usr/bin/env python # Author: Leonardo Gama (@leogama) # Copyright (c) 2022-2023 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/dill/blob/master/LICENSE import atexit import os import sys import __main__ from contextlib import suppress from io import BytesIO import dill session_file = os.path.join(os.path.dirname(__file__), 'session-refimported-%s.pkl') ################### # Child process # ################### def _error_line(error, obj, refimported): import traceback line = traceback.format_exc().splitlines()[-2].replace('[obj]', '['+repr(obj)+']') return "while testing (with refimported=%s): %s" % (refimported, line.lstrip()) if __name__ == '__main__' and len(sys.argv) >= 3 and sys.argv[1] == '--child': # Test session loading in a fresh interpreter session. refimported = (sys.argv[2] == 'True') dill.load_module(session_file % refimported, module='__main__') def test_modules(refimported): # FIXME: In this test setting with CPython 3.7, 'calendar' is not included # in sys.modules, independent of the value of refimported. Tried to # run garbage collection just before loading the session with no luck. It # fails even when preceding them with 'import calendar'. Needed to run # these kinds of tests in a supbrocess. Failing test sample: # assert globals()['day_name'] is sys.modules['calendar'].__dict__['day_name'] try: for obj in ('json', 'url', 'local_mod', 'sax', 'dom'): assert globals()[obj].__name__ in sys.modules assert 'calendar' in sys.modules and 'cmath' in sys.modules import calendar, cmath for obj in ('Calendar', 'isleap'): assert globals()[obj] is sys.modules['calendar'].__dict__[obj] assert __main__.day_name.__module__ == 'calendar' if refimported: assert __main__.day_name is calendar.day_name assert __main__.complex_log is cmath.log except AssertionError as error: error.args = (_error_line(error, obj, refimported),) raise test_modules(refimported) sys.exit() #################### # Parent process # #################### # Create various kinds of objects to test different internal logics. ## Modules. import json # top-level module import urllib as url # top-level module under alias from xml import sax # submodule import xml.dom.minidom as dom # submodule under alias import test_dictviews as local_mod # non-builtin top-level module ## Imported objects. from calendar import Calendar, isleap, day_name # class, function, other object from cmath import log as complex_log # imported with alias ## Local objects. x = 17 empty = None names = ['Alice', 'Bob', 'Carol'] def squared(x): return x**2 cubed = lambda x: x**3 class Person: def __init__(self, name, age): self.name = name self.age = age person = Person(names[0], x) class CalendarSubclass(Calendar): def weekdays(self): return [day_name[i] for i in self.iterweekdays()] cal = CalendarSubclass() selfref = __main__ # Setup global namespace for session saving tests. class TestNamespace: test_globals = globals().copy() def __init__(self, **extra): self.extra = extra def __enter__(self): self.backup = globals().copy() globals().clear() globals().update(self.test_globals) globals().update(self.extra) return self def __exit__(self, *exc_info): globals().clear() globals().update(self.backup) def _clean_up_cache(module): cached = module.__file__.split('.', 1)[0] + '.pyc' cached = module.__cached__ if hasattr(module, '__cached__') else cached pycache = os.path.join(os.path.dirname(module.__file__), '__pycache__') for remove, file in [(os.remove, cached), (os.removedirs, pycache)]: with suppress(OSError): remove(file) atexit.register(_clean_up_cache, local_mod) def _test_objects(main, globals_copy, refimported): try: main_dict = __main__.__dict__ global Person, person, Calendar, CalendarSubclass, cal, selfref for obj in ('json', 'url', 'local_mod', 'sax', 'dom'): assert globals()[obj].__name__ == globals_copy[obj].__name__ for obj in ('x', 'empty', 'names'): assert main_dict[obj] == globals_copy[obj] for obj in ['squared', 'cubed']: assert main_dict[obj].__globals__ is main_dict assert main_dict[obj](3) == globals_copy[obj](3) assert Person.__module__ == __main__.__name__ assert isinstance(person, Person) assert person.age == globals_copy['person'].age assert issubclass(CalendarSubclass, Calendar) assert isinstance(cal, CalendarSubclass) assert cal.weekdays() == globals_copy['cal'].weekdays() assert selfref is __main__ except AssertionError as error: error.args = (_error_line(error, obj, refimported),) raise def test_session_main(refimported): """test dump/load_module() for __main__, both in this process and in a subprocess""" extra_objects = {} if refimported: # Test unpickleable imported object in main. from sys import flags extra_objects['flags'] = flags with TestNamespace(**extra_objects) as ns: try: # Test session loading in a new session. dill.dump_module(session_file % refimported, refimported=refimported) from dill.tests.__main__ import python, shell, sp error = sp.call([python, __file__, '--child', str(refimported)], shell=shell) if error: sys.exit(error) finally: with suppress(OSError): os.remove(session_file % refimported) # Test session loading in the same session. session_buffer = BytesIO() dill.dump_module(session_buffer, refimported=refimported) session_buffer.seek(0) dill.load_module(session_buffer, module='__main__') ns.backup['_test_objects'](__main__, ns.backup, refimported) def test_session_other(): """test dump/load_module() for a module other than __main__""" import test_classdef as module atexit.register(_clean_up_cache, module) module.selfref = module dict_objects = [obj for obj in module.__dict__.keys() if not obj.startswith('__')] session_buffer = BytesIO() dill.dump_module(session_buffer, module) for obj in dict_objects: del module.__dict__[obj] session_buffer.seek(0) dill.load_module(session_buffer, module) assert all(obj in module.__dict__ for obj in dict_objects) assert module.selfref is module def test_runtime_module(): from types import ModuleType modname = '__runtime__' runtime = ModuleType(modname) runtime.x = 42 mod = dill.session._stash_modules(runtime) if mod is not runtime: print("There are objects to save by referenece that shouldn't be:", mod.__dill_imported, mod.__dill_imported_as, mod.__dill_imported_top_level, file=sys.stderr) # This is also for code coverage, tests the use case of dump_module(refimported=True) # without imported objects in the namespace. It's a contrived example because # even dill can't be in it. This should work after fixing #462. session_buffer = BytesIO() dill.dump_module(session_buffer, module=runtime, refimported=True) session_dump = session_buffer.getvalue() # Pass a new runtime created module with the same name. runtime = ModuleType(modname) # empty return_val = dill.load_module(BytesIO(session_dump), module=runtime) assert return_val is None assert runtime.__name__ == modname assert runtime.x == 42 assert runtime not in sys.modules.values() # Pass nothing as main. load_module() must create it. session_buffer.seek(0) runtime = dill.load_module(BytesIO(session_dump)) assert runtime.__name__ == modname assert runtime.x == 42 assert runtime not in sys.modules.values() def test_refimported_imported_as(): import collections import concurrent.futures import types import typing mod = sys.modules['__test__'] = types.ModuleType('__test__') dill.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) mod.Dict = collections.UserDict # select by type mod.AsyncCM = typing.AsyncContextManager # select by __module__ mod.thread_exec = dill.executor # select by __module__ with regex session_buffer = BytesIO() dill.dump_module(session_buffer, mod, refimported=True) session_buffer.seek(0) mod = dill.load(session_buffer) del sys.modules['__test__'] assert set(mod.__dill_imported_as) == { ('collections', 'UserDict', 'Dict'), ('typing', 'AsyncContextManager', 'AsyncCM'), ('dill', 'executor', 'thread_exec'), } def test_load_module_asdict(): with TestNamespace(): session_buffer = BytesIO() dill.dump_module(session_buffer) global empty, names, x, y x = y = 0 # change x and create y del empty globals_state = globals().copy() session_buffer.seek(0) main_vars = dill.load_module_asdict(session_buffer) assert main_vars is not globals() assert globals() == globals_state assert main_vars['__name__'] == '__main__' assert main_vars['names'] == names assert main_vars['names'] is not names assert main_vars['x'] != x assert 'y' not in main_vars assert 'empty' in main_vars if __name__ == '__main__': test_session_main(refimported=False) test_session_main(refimported=True) test_session_other() test_runtime_module() test_refimported_imported_as() test_load_module_asdict()