Repository: pydantic/pydantic
Pull request: Add _types_namespace argument to model_rebuild
Closes https://github.com/pydantic/pydantic/issues/5383

I called the argument `_types_namespace` instead of `_types_namespace` to try to discourage people from using it (like `_parent_namespace_depth`, you probably shouldn't change the value unless you know what you are doing).

But I would be open to changing this (and `_parent_namespace_depth`) to take the more standard approach of not including a leading `_` if anyone prefers.

@commonism If the unit test I added here isn't representative of your usage (e.g., if you use `create_model` to create the classes you mentioned in #5383) could you share a more representative unit test and/or some code that shows how you use this functionality? Want to make sure this change actually fixes things for you (and it doesn't get re-broken in the future).

Selected Reviewer: @adriangb

Problem statement:
`localns` arguments are not longer accepted.
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

I used localns to create nested models dynamically using pydanticv1.
Define all models, `….update_forward_refs({"ModelNameA":ModelA","ModelNameA":ModelA",})` for all of them in the proper order and it works.

How to without localns using pydanticv2?


### Example Code

_No response_

### Python, Pydantic & OS Version

```Text
pydantic version: 2.0a1
                 install path: ~/venv/openapi3/lib/python3.10/site-packages/pydantic
               python version: 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0]
                     platform: Linux-5.19.0-35-generic-x86_64-with-glibc2.35
     optional deps. installed: ['email-validator', 'typing-extensions']
```

Hints from issue discussion:
Yeah, I realized we had eliminated this and was concerned someone might want the ability to specify the localns directly. I think we can re-add this into `model_rebuild`, I'll take a stab at it.
@commonism just opened #5385 to address this; as I noted there:

> @commonism If the unit test I added here isn't representative of your usage (e.g., if you use create_model to create the classes you mentioned in https://github.com/pydantic/pydantic/issues/5383) could you share a more representative unit test and/or some code that shows how you use this functionality? Want to make sure this change actually fixes things for you (and it doesn't get re-broken in the future).

Commit message: Add _types_namespace argument to model_rebuild

Patch to review:
diff --git a/pydantic/main.py b/pydantic/main.py
index 762084ae027..03f63547319 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -389,6 +389,7 @@ def model_rebuild(
         force: bool = False,
         raise_errors: bool = True,
         _parent_namespace_depth: int = 2,
+        _types_namespace: dict[str, Any] | None = None,
     ) -> bool | None:
         """
         Try to (Re)construct the model schema.
@@ -396,14 +397,17 @@ def model_rebuild(
         if not force and cls.__pydantic_model_complete__:
             return None
         else:
-            if _parent_namespace_depth > 0:
-                frame_parent_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth) or {}
-                cls_parent_ns = cls.__pydantic_parent_namespace__ or {}
-                cls.__pydantic_parent_namespace__ = {**cls_parent_ns, **frame_parent_ns}
+            if _types_namespace is not None:
+                types_namespace: dict[str, Any] | None = _types_namespace.copy()
+            else:
+                if _parent_namespace_depth > 0:
+                    frame_parent_ns = _typing_extra.parent_frame_namespace(parent_depth=_parent_namespace_depth) or {}
+                    cls_parent_ns = cls.__pydantic_parent_namespace__ or {}
+                    cls.__pydantic_parent_namespace__ = {**cls_parent_ns, **frame_parent_ns}
 
-            types_namespace = cls.__pydantic_parent_namespace__
+                types_namespace = cls.__pydantic_parent_namespace__
 
-            types_namespace = _model_construction.get_model_types_namespace(cls, types_namespace)
+                types_namespace = _model_construction.get_model_types_namespace(cls, types_namespace)
             return _model_construction.complete_model_class(
                 cls,
                 cls.__name__,
diff --git a/tests/test_main.py b/tests/test_main.py
index 772ffc0d3bf..ce27dbb87f0 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1839,6 +1839,20 @@ class C(BaseModel, undefined_types_warning=False):
     assert m.model_dump() == {'b': {'c': {'a': None}}}
 
 
+def test_model_rebuild_localns():
+    class A(BaseModel, undefined_types_warning=False):
+        x: int
+
+    class B(BaseModel, undefined_types_warning=False):
+        a: 'Model'  # noqa F821
+
+    B.model_rebuild(_types_namespace={'Model': A})
+
+    m = B(a={'x': 1})
+    assert m.model_dump() == {'a': {'x': 1}}
+    assert isinstance(m.a, A)
+
+
 @pytest.fixture(scope='session', name='InnerEqualityModel')
 def inner_equality_fixture():
     class InnerEqualityModel(BaseModel):


Code review comments:
diff_hunk: @@ -1839,6 +1839,20 @@ class C(BaseModel, undefined_types_warning=False):
     assert m.model_dump() == {'b': {'c': {'a': None}}}
 
 
+def test_model_rebuild_localns():
+    class A(BaseModel, undefined_types_warning=False):
+        x: int
+
+    class B(BaseModel, undefined_types_warning=False):
+        a: 'Model'  # noqa F821
+
+    B.model_rebuild(_types_namespace={'Model': A})
path: tests/test_main.py
text: @user1:
negative testing
```python
    class C(BaseModel, undefined_types_warning=False):
        a: 'Model'  # noqa F821

    with pytest.raises(…):
        C.model_rebuild(_types_namespace={'A': A})
```