Repository: streamlit/streamlit

Issue:
Rerunning and pydantic validators
### Summary

Streamlit's rerun mechanism causes exceptions with pydantic validators.

### Steps to reproduce

Code snippet:

```python
import streamlit as st

from pydantic import BaseModel, validator

class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)

st.write(user.dict())
```
(pydantic code is simplified example from the [docs](https://pydantic-docs.helpmanual.io/usage/validators/)

1. Run streamlit app and it works as expected
![Screenshot 2021-05-05 at 11 57 08](https://user-images.githubusercontent.com/1042275/117131833-e34ce100-ad99-11eb-8044-de044116ebc6.png)

2. Make any change the source code (eg edit a field of the `user`) and rerun.  `pydantic` throws an error since `pydantic` thinks the validator is being reused
![Screenshot 2021-05-05 at 11 57 22](https://user-images.githubusercontent.com/1042275/117131859-ed6edf80-ad99-11eb-960d-becfdbecfcf9.png)

**Expected behavior:**

Expect the rerun mechanism wouldn't interfere with third party library (`pydantic`) and produce the same output above

**Actual behavior:**

`pydantic` throws a `ConfigError: duplicate validator function` as it thinks the validator function is being reused on the rerun.  See screenshot above of streamlit catching the exception

Changing the `validator` decorator to include the ` allow_reuse=True` [argument](https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators) fixes this issue.  However, changing code to allow streamlit to work is unwanted and it isn't always possible (eg if the pydantic model objects are created by a library the user doesn't have control over)

### Is this a regression?

I tried `streamlit==0.70.0` and the bug exists there.  I suspect this bug has always existed

### Debug info

- Streamlit version: `0.81.1`
- Python version: `3.9.4`
- Using PyEnv
- OS version: MacOS 11.3
- Browser version: Chrome Version 90.0.4430.93 (Official Build) (x86_64)


Discussion:
Thanks @luke321321 for the issue! We would need to investigate this one to provide an explanation on why it is happening. My guess is that we are not clearing the "cache" of the script intentionally, which in this case would be hard to resolve. In any case, I put this in a project to investigate. Thanks again!
Try to add this line at the very top of your file:
```python
import pydantic; pydantic.class_validators._FUNCS.clear()
```
@kmcgrady is there any road towards no needing to mention the hack of my previous comment?
I encountered the same problem and suffered from it for a long time, but after following the internal mechanism of streamlit, I found that if it is possible to separate the files, it is better to do the following:

mymodule/models/user.py
```python
from pydantic import BaseModel, validator

class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()
```

mymodule/main.py
```python
import streamlit as st

from mymodule.models.user import UserModel

user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)

st.write(user.dict())
```

.streamlit/config.toml

```toml
[server]
folderWatchBlacklist = [
    "**/mymodule/models"
]
```

Python normally does not reload modules once they have been loaded, but streamlit removes the values of already loaded modules from `sys.modules` each time it performs rerun in order to immediately reflect changes related to the code being watched.
This causes the same library to be loaded many times, resulting in multiple initialization of variables in the static area, which causes this problem.

I think a reasonable solution at present is to exclude the processes that cause these problems from the folderWatch. It would be good if this is explicitly stated in the streamlit docs.

Resolving patch:
diff --git a/lib/streamlit/web/bootstrap.py b/lib/streamlit/web/bootstrap.py
--- a/lib/streamlit/web/bootstrap.py
+++ b/lib/streamlit/web/bootstrap.py
@@ -197,6 +197,31 @@ def _fix_pydeck_mapbox_api_warning() -> None:
     os.environ["MAPBOX_API_KEY"] = config.get_option("mapbox.token")
 
 
+def _fix_pydantic_duplicate_validators_error():
+    """Pydantic by default disallows to reuse of validators with the same name,
+    this combined with the Streamlit execution model leads to an error on the second
+    Streamlit script rerun if the Pydantic validator is registered
+    in the streamlit script.
+
+    It is important to note that the same issue exists for Pydantic validators inside
+    Jupyter notebooks, https://github.com/pydantic/pydantic/issues/312 and in order
+    to fix that in Pydantic they use the `in_ipython` function that checks that
+    Pydantic runs not in `ipython` environment.
+
+    Inside this function we patch `in_ipython` function to always return `True`.
+
+    This change will relax rules for writing Pydantic validators inside
+    Streamlit script a little bit, similar to how it works in jupyter,
+    which should not be critical.
+    """
+    try:
+        from pydantic import class_validators
+
+        class_validators.in_ipython = lambda: True  # type: ignore[attr-defined]
+    except ImportError:
+        pass
+
+
 def _print_new_version_message() -> None:
     if version.should_show_new_version_notice():
         click.secho(NEW_VERSION_TEXT)
@@ -381,6 +406,7 @@ def run(
     _fix_tornado_crash()
     _fix_sys_argv(main_script_path, args)
     _fix_pydeck_mapbox_api_warning()
+    _fix_pydantic_duplicate_validators_error()
     _install_config_watchers(flag_options)
     _install_pages_watcher(main_script_path)
 


Test patch:
diff --git a/lib/tests/streamlit/web/bootstrap_test.py b/lib/tests/streamlit/web/bootstrap_test.py
--- a/lib/tests/streamlit/web/bootstrap_test.py
+++ b/lib/tests/streamlit/web/bootstrap_test.py
@@ -18,13 +18,17 @@
 from unittest.mock import Mock, patch
 
 import matplotlib
+import pytest
 
 from streamlit import config
 from streamlit.web import bootstrap
-from streamlit.web.bootstrap import NEW_VERSION_TEXT
+from streamlit.web.bootstrap import (
+    NEW_VERSION_TEXT,
+    _fix_pydantic_duplicate_validators_error,
+)
 from tests import testutil
 from tests.isolated_asyncio_test_case import IsolatedAsyncioTestCase
-from tests.testutil import patch_config_options
+from tests.testutil import patch_config_options, should_skip_pydantic_tests
 
 
 class BootstrapTest(unittest.TestCase):
@@ -60,6 +64,55 @@ def test_fix_matplotlib_crash(self):
         sys.platform = ORIG_PLATFORM
 
 
+class BootstrapPydanticFixTest(unittest.TestCase):
+    def pydantic_model_definition(self):
+        from pydantic import BaseModel, root_validator, validator
+
+        class UserModel(BaseModel):
+            name: str
+            username: str
+            password1: str
+            password2: str
+
+            @validator("name")
+            def name_must_contain_space(cls, v):
+                if " " not in v:
+                    raise ValueError("must contain a space")
+                return v.title()
+
+            @root_validator()
+            def passwords_should_match(cls, values):
+                if values["password1"] != values["password2"]:
+                    raise ValueError("passwords do not match")
+                return values
+
+        UserModel(
+            name="John Doe",
+            username="johndoe",
+            password1="abcd",
+            password2="abcd",
+        )
+
+    @pytest.mark.skipif(
+        should_skip_pydantic_tests(), reason="We test fix only for pydantic 1.*"
+    )
+    @patch("pydantic.class_validators.in_ipython", Mock(return_value=False))
+    def test_fix_pydantic_crash(self):
+        import pydantic
+
+        # Check that without fix it crashes when model with validator
+        # defined two times (we emulate Streamlit rerun).
+        with self.assertRaises(pydantic.errors.ConfigError):
+            self.pydantic_model_definition()
+            self.pydantic_model_definition()
+
+        _fix_pydantic_duplicate_validators_error()
+
+        # Check that after fix model could be redefined without exception.
+        self.pydantic_model_definition()
+        self.pydantic_model_definition()
+
+
 class BootstrapPrintTest(IsolatedAsyncioTestCase):
     """Test bootstrap.py's printing functions.
 
diff --git a/lib/tests/testutil.py b/lib/tests/testutil.py
--- a/lib/tests/testutil.py
+++ b/lib/tests/testutil.py
@@ -39,6 +39,15 @@ def should_skip_pyspark_tests() -> bool:
     return sys.version_info >= (3, 11, 0)
 
 
+def should_skip_pydantic_tests() -> bool:
+    try:
+        import pydantic
+
+        return not pydantic.__version__.startswith("1.")
+    except ImportError:
+        return True
+
+
 def create_mock_script_run_ctx() -> ScriptRunContext:
     """Create a ScriptRunContext for use in tests."""
     return ScriptRunContext(