Repository: lmfit/lmfit-py

Issue:
Sys.module needs to be copied before being iterated over
#### Description
According to https://docs.python.org/3/library/sys.html#sys.modules, one needs to copy `sys.module` to iterate over it. This function [in this file](https://github.com/lmfit/lmfit-py/blob/8781a2dc33288b25fae6f3139595402155e4968b/lmfit/jsonutils.py#L27) does not follow this advice.

It should be replaced by:
```
def find_importer(obj):
    """Find importer of an object."""
    oname = obj.__name__
    for modname, module in sys.modules.copy().items():
        if modname.startswith('__main__'):
            continue
        t = getattr(module, oname, None)
        if t is obj:
            return modname
    return None
```

Please note `sys.modules.items()` -> `sys.modules.copy().items()`

###### A Minimal, Complete, and Verifiable example
I ran into `RuntimeError: dictionary changed size during iteration` with a large project using lmfit. The solution indicated in the python docs solves it. It is not immediate to create an MVE and I hope the python docs is enough to convince to implement the change.

###### Error message:
<!-- If any, paste the *full* error message inside a code block (starting from line Traceback) -->

```
...

obj = <function test_serde_lmfit_modelresult_with_error.<locals>.custom_error at 0x308a292d0>

    def find_importer(obj):
        """Find importer of an object."""
        oname = obj.__name__
>       for modname, module in sys.modules.items():
E       RuntimeError: dictionary changed size during iteration

/.../lib/python3.10/site-packages/lmfit/jsonutils.py:27: RuntimeError
```

###### Version information
Python: 3.10.14 (main, Mar 19 2024, 21:46:16) [Clang 15.0.0 (clang-1500.3.9.4)]

lmfit: 1.3.0, scipy: 1.11.4, numpy: 1.26.3,asteval: 0.9.32, uncertainties: 3.1.6###### Link(s)
<!-- If you started a discussion on the lmfit mailing list, discussion page, or Stack Overflow, please provide the relevant link(s) -->



Discussion:
thanks @jcjaskula-aws for pointing that out. We indeed don't follow the advice in the Python documentation and should/will change to do so as far as I am concerned. Interestingly, nobody has noticed an issue with it in the last 6 years when this code was introduced - so I guess it isn't that common. Anyway, let's see what @newville thinks but this seems uncontroversial to me!
@jcjaskula-aws @reneeotten I agree.  I would say all of

1.  hm, yeah I can see why that is recommended, and we should change the code to use a copy there.
2. hm, it is kind of hard to understand why this loop over getting attributes could alter `sys.modules`.   Is some other thread doing imports while you're saving a model?   
4. hm, it is kind of amazing that this hasn't been seen before.

Anyway, +1 on changing this.  Maybe you'd be willing to make a PR?

Resolving patch:
diff --git a/lmfit/jsonutils.py b/lmfit/jsonutils.py
--- a/lmfit/jsonutils.py
+++ b/lmfit/jsonutils.py
@@ -24,7 +24,7 @@
 def find_importer(obj):
     """Find importer of an object."""
     oname = obj.__name__
-    for modname, module in sys.modules.items():
+    for modname, module in sys.modules.copy().items():
         if modname.startswith('__main__'):
             continue
         t = getattr(module, oname, None)


Test patch: