Repository: pydantic/pydantic

Issue:
Parse default value of ImportString
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

There is a mention in documentation:

> A known limitation: setting a default value to a string won't result in validation (thus evaluation). This is actively being worked on.

As of pydantic 2.5.0 the issue is still present. And I don't see any open issue or pull request related to this. 

### Example Code

```Python
import math
from pydantic import BaseModel, ImportString

class MyModel(BaseModel):
  attr: ImportString = "math.cos"

assert MyModel().attr == "math.cos"  # not parsed
assert MyModel(attr="math.sin").attr == math.sin  # parsed
```

[Here](https://github.com/pydantic/pydantic/pull/6456) I see an proposal like:
```python
from pydantic import BaseModel, ImportString

class MyModel(BaseModel):
  module = ImportString.import_string('os.path')  # equivalent to `module = os.path`
```

Which does not work:
```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in ImportStringModel
AttributeError: type object 'ImportString' has no attribute 'import_string'
```

ImportString also does not support passing import string as constructor argument:
```python
>>> ImportString('os.path')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ImportString() takes no arguments
```

The only working workaround I get is this:
```python
from pydantic import BaseModel, ImportString, validator

class MyModel(BaseModel):
    attr: ImportString = ""

    @validator("attr", always=True, pre=True)
    def _default_sttr_value(cls, value):
        if value:
            return value

        import math
        return math.cos

assert MyModel().attr == math.cos
assert MyModel(attr="math.sin").attr == math.sin

```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.5.0
        pydantic-core version: 2.14.1
          pydantic-core build: profile=release pgo=true
                 install path: /home/maxim/Repo/horizon/.venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.5 (main, Sep  2 2023, 14:16:33) [GCC 13.2.1 20230801]
                     platform: Linux-6.6.0-1-MANJARO-x86_64-with-glibc2.38
             related packages: mypy-1.7.0 pydantic-settings-2.0.3 fastapi-0.103.2 typing_extensions-4.7.1
```



Discussion:
Hi @dolfinus,

You can use the `validate_default` flag to resolve this issue.

```py
import math
from pydantic import BaseModel, Field, ImportString

class MyModel(BaseModel):
  attr: ImportString = Field(default="math.cos", validate_default=True)

assert MyModel().attr == math.cos  # parsed
assert MyModel(attr="math.sin").attr == math.sin  # parsed
```
Could you please update documentation to include this?
I think this is a great idea. @dolfinus, could you please open a PR with this change to the docs? 🌟 

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -887,15 +887,11 @@ class ImportString:
         On model instantiation, pointers will be evaluated and imported. There is
         some nuance to this behavior, demonstrated in the examples below.
 
-        > A known limitation: setting a default value to a string
-        > won't result in validation (thus evaluation). This is actively
-        > being worked on.
-
         **Good behavior:**
         ```py
         from math import cos
 
-        from pydantic import BaseModel, ImportString, ValidationError
+        from pydantic import BaseModel, Field, ImportString, ValidationError
 
 
         class ImportThings(BaseModel):
@@ -925,7 +921,31 @@ class ImportThings(BaseModel):
         # Actual python objects can be assigned as well
         my_cos = ImportThings(obj=cos)
         my_cos_2 = ImportThings(obj='math.cos')
-        assert my_cos == my_cos_2
+        my_cos_3 = ImportThings(obj='math:cos')
+        assert my_cos == my_cos_2 == my_cos_3
+
+
+        # You can set default field value either as Python object:
+        class ImportThingsDefaultPyObj(BaseModel):
+            obj: ImportString = math.cos
+
+
+        # or as a string value (but only if used with `validate_default=True`)
+        class ImportThingsDefaultString(BaseModel):
+            obj: ImportString = Field(default='math.cos', validate_default=True)
+
+
+        my_cos_default1 = ImportThingsDefaultPyObj()
+        my_cos_default2 = ImportThingsDefaultString()
+        assert my_cos_default1.obj == my_cos_default2.obj == math.cos
+
+
+        # note: this will not work!
+        class ImportThingsMissingValidateDefault(BaseModel):
+            obj: ImportString = 'math.cos'
+
+        my_cos_default3 = ImportThingsMissingValidateDefault()
+        assert my_cos_default3.obj == 'math.cos'  # just string, not evaluated
         ```
 
         Serializing an `ImportString` type to json is also possible.
@@ -939,7 +959,7 @@ class ImportThings(BaseModel):
 
 
         # Create an instance
-        m = ImportThings(obj='math:cos')
+        m = ImportThings(obj='math.cos')
         print(m)
         #> obj=<built-in function cos>
         print(m.model_dump_json())


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -877,6 +877,8 @@ class PyObjectModel(BaseModel):
     [
         ('math:cos', 'math.cos', 'json'),
         ('math:cos', math.cos, 'python'),
+        ('math.cos', 'math.cos', 'json'),
+        ('math.cos', math.cos, 'python'),
         pytest.param(
             'os.path', 'posixpath', 'json', marks=pytest.mark.skipif(sys.platform == 'win32', reason='different output')
         ),
@@ -903,6 +905,22 @@ class PyObjectModel(BaseModel):
     assert PyObjectModel(thing=value).model_dump(mode=mode) == {'thing': expected}
 
 
+@pytest.mark.parametrize(
+    ('value', 'validate_default', 'expected'),
+    [
+        (math.cos, True, math.cos),
+        ('math:cos', True, math.cos),
+        (math.cos, False, math.cos),
+        ('math:cos', False, 'math:cos'),
+    ],
+)
+def test_string_import_default_value(value: Any, validate_default: bool, expected: Any):
+    class PyObjectModel(BaseModel):
+        thing: ImportString = Field(default=value, validate_default=validate_default)
+
+    assert PyObjectModel().thing == expected
+
+
 @pytest.mark.parametrize('value', ['oss', 'os.os', f'{__name__}.x'])
 def test_string_import_any_expected_failure(value: Any):
     """Ensure importString correctly fails to instantiate when it's supposed to"""