Repository: samuelcolvin/pydantic (python)

Issue:
Issue with BaseModel if used with importlib 
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.7.3
            pydantic compiled: True
                 install path: /opt/conda/lib/python3.8/site-packages/pydantic
               python version: 3.8.2 (default, Mar 25 2020, 17:03:02)  [GCC 7.3.0]
                     platform: Linux-4.15.0-130-generic-x86_64-with-glibc2.10
     optional deps. installed: ['typing-extensions']
```

If I'm trying to load a module using `importlib` where a BaseModel is used then execution is crashed with the following traceback:
```
Traceback (most recent call last):
  File "loader.py", line 13, in <module>
    m = load_module("module_to_load.py")
  File "loader.py", line 7, in load_module
    spec.loader.exec_module(module)  # type: ignore[union-attr]
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "module_to_load.py", line 4, in <module>
    class User(BaseModel):
  File "pydantic/main.py", line 274, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/typing.py", line 293, in pydantic.typing.resolve_annotations
    def __copy__(self):
```

How to repro:
1) create `loader.py` file with the following content
```python
import importlib.util

def load_module(fpath):
    spec = importlib.util.spec_from_file_location("mymodule", fpath)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)  # type: ignore[union-attr]
    return module

if __name__ == "__main__":    
    m = load_module("module_to_load.py")
    print(m)
    m.foo()
```

2) create `module_to_load.py` : 
```python
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'Jane Doe'

def foo():
    print(User(id=12, name="test"))
```

3) Run `python loader.py`


As use-case may seem strange, I'm a bit hestiating between a bug and wrong usage, but in the latter case, error message is unclear at all. Important part is that I have to import the module in this way and not in any other more standard python way in which pydantic indeed work without any issue.
Thanks 


EDIT:

Indeed, if we add `sys.modules["mymodule"] = module` to `load_module` as suggested [here](https://docs.python.org/3/library/importlib.html?highlight=exec_module#importing-a-source-file-directly), there is no issue, anymore.
I have to figure out what would be the impact of doing that for my goal.


Pull request:
make "resolve_annotations" more lenient, allowing for missing modules
Make `resolve_annotations` more lenient, allowing for missing modules

## Related issue number

fix #2363 

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/2363-samuelcolvin.md b/changes/2363-samuelcolvin.md
new file mode 100644
index 000000000..68c221ab6
--- /dev/null
+++ b/changes/2363-samuelcolvin.md
@@ -0,0 +1,1 @@
+Make `resolve_annotations` more lenient, allowing for missing modules
diff --git a/pydantic/typing.py b/pydantic/typing.py
index 314faef43..36cd1fb1d 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -289,10 +289,16 @@ def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Opti
 
     Resolve string or ForwardRef annotations into type objects if possible.
     """
+    base_globals: Optional[Dict[str, Any]] = None
     if module_name:
-        base_globals: Optional[Dict[str, Any]] = sys.modules[module_name].__dict__
-    else:
-        base_globals = None
+        try:
+            module = sys.modules[module_name]
+        except KeyError:
+            # happens occasionally, see https://github.com/samuelcolvin/pydantic/issues/2363
+            pass
+        else:
+            base_globals = module.__dict__
+
     annotations = {}
     for name, value in raw_annotations.items():
         if isinstance(value, str):


Test patch:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index 5ad7347d1..09e1bac76 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -1,3 +1,4 @@
+import importlib.util
 import sys
 from collections.abc import Hashable
 from decimal import Decimal
@@ -1773,3 +1774,22 @@ return Model
     assert model.a == 10.2
     assert model.b == 10
     return model.get_double_a() == 20.2
+
+
+def test_resolve_annotations_module_missing(tmp_path):
+    # see https://github.com/samuelcolvin/pydantic/issues/2363
+    file_path = tmp_path / 'module_to_load.py'
+    # language=Python
+    file_path.write_text(
+        """
+from pydantic import BaseModel
+class User(BaseModel):
+    id: int
+    name = 'Jane Doe'
+"""
+    )
+
+    spec = importlib.util.spec_from_file_location('my_test_module', file_path)
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    assert module.User(id=12).dict() == {'id': 12, 'name': 'Jane Doe'}