diff --git a/doc/model.rst b/doc/model.rst index 43733cd5..b38254cd 100644 --- a/doc/model.rst +++ b/doc/model.rst @@ -589,15 +589,22 @@ emphasized that if you are willing to save or reuse the definition of the model function as Python code, then saving the Parameters and rest of the components that make up a model presents no problem. -If the ``dill`` package is installed, the model function will be saved using -it. But because saving the model function is not always reliable, saving a -model will always save the *name* of the model function. The :func:`load_model` -takes an optional :attr:`funcdefs` argument that can contain a dictionary of -function definitions with the function names as keys and function objects as -values. If one of the dictionary keys matches the saved name, the -corresponding function object will be used as the model function. With this -approach, if you save a model and can provide the code used for the model -function, the model can be saved and reliably reloaded and used. +If the ``dill`` package is installed, the model function will also be saved +using it. But because saving the model function is not always reliable, +saving a model will always save the *name* of the model function. The +:func:`load_model` takes an optional :attr:`funcdefs` argument that can +contain a dictionary of function definitions with the function names as +keys and function objects as values. If one of the dictionary keys matches +the saved name, the corresponding function object will be used as the model +function. If it is not found by name, and if ``dill`` was used to save +the model, and if ``dill`` is available at run-time, the ``dill``-encoded +function will try to be used. Note that this approach will generally allow +you to save a model that can be used by another installation of the +same version of Python, but may not work across Python versions. For preserving +fits for extended periods of time (say, archiving for documentation of +scientific results), we strongly encourage you to save the full Python code +used for the model function and fit process. + .. autofunction:: save_model diff --git a/doc/parameters.rst b/doc/parameters.rst index 08385683..ae82762b 100644 --- a/doc/parameters.rst +++ b/doc/parameters.rst @@ -81,6 +81,16 @@ The :class:`Parameters` class .. automethod:: load +.. _dumpload_warning: + +.. warning:: + + Saving Parameters with user-added functions to the ``_asteval`` + interpreter using :meth::`dump` and :meth:`dumps` may not be easily + recovered with the :meth:`load` and :meth:`loads`. See + :ref:`model_saveload_sec` for further discussion. + + Simple Example ============== diff --git a/lmfit/jsonutils.py b/lmfit/jsonutils.py index 80e7477c..7e91afa2 100644 --- a/lmfit/jsonutils.py +++ b/lmfit/jsonutils.py @@ -2,6 +2,7 @@ from base64 import b64decode, b64encode import sys +import warnings import numpy as np @@ -88,14 +89,10 @@ def encode4js(obj): out[encode4js(key)] = encode4js(val) return out if callable(obj): - val, importer = None, None - if HAS_DILL: - val = str(b64encode(dill.dumps(obj)), 'utf-8') - else: - val = None - importer = find_importer(obj) + value = str(b64encode(dill.dumps(obj)), 'utf-8') if HAS_DILL else None return dict(__class__='Callable', __name__=obj.__name__, - pyversion=pyvers, value=val, importer=importer) + pyversion=pyvers, value=value, + importer=find_importer(obj)) return obj @@ -132,11 +129,19 @@ def decode4js(obj): elif classname == 'PSeries' and read_json is not None: out = read_json(obj['value'], typ='series') elif classname == 'Callable': - out = val = obj['__name__'] - if pyvers == obj['pyversion'] and HAS_DILL: - out = dill.loads(b64decode(obj['value'])) - elif obj['importer'] is not None: - out = import_from(obj['importer'], val) + out = obj['__name__'] + try: + out = import_from(obj['importer'], out) + unpacked = True + except (ImportError, AttributeError): + unpacked = False + if not unpacked and HAS_DILL: + try: + out = dill.loads(b64decode(obj['value'])) + except RuntimeError: + msg = "Could not unpack dill-encoded callable `{0}`, saved with Python version {1}" + warnings.warn(msg.format(obj['__name__'], + obj['pyversion'])) elif classname in ('Dict', 'dict'): out = {}