Repository: pydantic/pydantic

Issue:
UUID typing hints of Pydantic 1.8 are inaccurate
### 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

It appears commit 7da04d95fa4a7b473a9dae4d1ab9d7eba98a9aa9 introduces a type erasure on `UUID1`/`UUID2`/`UUID3`/`UUID4` that makes them all `Union[UUID, str]`, losing the strict inheritance from `UUID`.

Now:

```python
from uuid import UUID, uuid4
from pydantic import UUID4, BaseModel

def expects_any_uuid(arg: UUID):
    pass

class MyModel(BaseModel):
    my_uuid: UUID4


# While these two are now correct in regards to type checking:
# (they were triggering typing errors in v1.7)
MyModel(my_uuid=uuid4())                                 # 1
MyModel(my_uuid="5259A9AA-D1FD-4D0A-B17D-87803904C233")  # 2

# These two calls are not correct anymore:
expects_any_uuid(MyModel(my_uuid=uuid4()).my_uuid)       # 3
expects_any_uuid(uuid4())                                # 4
# Type checking like MyPy would say something along the following:
#   Argument 1 to "expects_any_uuid" has incompatible type "Union[UUID, str]"; expected "UUID"
```

I would argue that making `# 2` clear the typing error from 1.7 is nice to have, but we could already use something like a `# type: ignore` comment to explicitly tell type-checking, which was acceptable since some `str`s are not `UUID4`s, and as such it makes sense to have to explicitly silent a warning here.

Same with `# 1`, since its type is “erased” to a generic `UUID`, we would need to check again that it is indeed a v4 UUID.

On the other hand, a `UUID4` is always a `UUID`, so `# 3` and `# 4` should never trigger a static typing error. 

I don't have a solution to make all examples `# 1` to `# 4` from above work, so instead I'm opening an issue here in hopes someone would have a solution.

---

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.8
            pydantic compiled: True
                 install path: <redacted>/venv/lib/python3.9/site-packages/pydantic
               python version: 3.9.2 (default, Feb 19 2021, 17:43:04)  [Clang 12.0.0 (clang-1200.0.32.29)]
                     platform: macOS-11.2.1-x86_64-i386-64bit
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->




Discussion:

Resolving patch:
diff --git a/pydantic/_hypothesis_plugin.py b/pydantic/_hypothesis_plugin.py
--- a/pydantic/_hypothesis_plugin.py
+++ b/pydantic/_hypothesis_plugin.py
@@ -139,10 +139,10 @@ def add_luhn_digit(card_number: str) -> str:
 )
 
 # UUIDs
-st.register_type_strategy(pydantic.UUID1, st.uuids(version=1))  # type: ignore[arg-type]
-st.register_type_strategy(pydantic.UUID3, st.uuids(version=3))  # type: ignore[arg-type]
-st.register_type_strategy(pydantic.UUID4, st.uuids(version=4))  # type: ignore[arg-type]
-st.register_type_strategy(pydantic.UUID5, st.uuids(version=5))  # type: ignore[arg-type]
+st.register_type_strategy(pydantic.UUID1, st.uuids(version=1))
+st.register_type_strategy(pydantic.UUID3, st.uuids(version=3))
+st.register_type_strategy(pydantic.UUID4, st.uuids(version=4))
+st.register_type_strategy(pydantic.UUID5, st.uuids(version=5))
 
 # Secrets
 st.register_type_strategy(pydantic.SecretBytes, st.binary().map(pydantic.SecretBytes))
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -517,8 +517,6 @@ def conlist(item_type: Type[T], *, min_items: int = None, max_items: int = None)
 
 
 if TYPE_CHECKING:
-    # TODO: add `str` and support it thanks to the plugin
-    # PyObject = Union[str, Callable[..., Any]]
     PyObject = Callable[..., Any]
 else:
 
@@ -632,10 +630,10 @@ def condecimal(
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UUID TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 if TYPE_CHECKING:
-    UUID1 = Union[UUID, str]
-    UUID3 = Union[UUID, str]
-    UUID4 = Union[UUID, str]
-    UUID5 = Union[UUID, str]
+    UUID1 = UUID
+    UUID3 = UUID
+    UUID4 = UUID
+    UUID5 = UUID
 else:
 
     class UUID1(UUID):
@@ -658,8 +656,8 @@ class UUID5(UUID1):
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PATH TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 if TYPE_CHECKING:
-    FilePath = Union[Path, str]
-    DirectoryPath = Union[Path, str]
+    FilePath = Path
+    DirectoryPath = Path
 else:
 
     class FilePath(Path):


Test patch:
diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py
--- a/tests/mypy/modules/success.py
+++ b/tests/mypy/modules/success.py
@@ -208,15 +208,29 @@ class PydanticTypes(BaseModel):
     # String
     my_strict_str: StrictStr = 'pika'
     # PyObject
-    # TODO: my_pyobject_str: PyObject = 'datetime.date'
+    my_pyobject_str: PyObject = 'datetime.date'  # type: ignore
     my_pyobject_callable: PyObject = date
     # UUID
     my_uuid1: UUID1 = UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')
-    my_uuid1_str: UUID1 = 'a8098c1a-f86e-11da-bd1a-00112444be1e'
+    my_uuid1_str: UUID1 = 'a8098c1a-f86e-11da-bd1a-00112444be1e'  # type: ignore
     # Path
-    my_file_path: FilePath = Path('root') / 'myfile.txt'
-    my_file_path_str: FilePath = 'root/myfile.txt'
-    my_dir_path: DirectoryPath = Path('root') / 'mydir'
-    my_dir_path_str: DirectoryPath = 'root/mydir'
+    my_file_path: FilePath = Path(__file__)
+    my_file_path_str: FilePath = __file__  # type: ignore
+    my_dir_path: DirectoryPath = Path('.')
+    my_dir_path_str: DirectoryPath = '.'  # type: ignore
     # Json
     my_json: Json = '{"hello": "world"}'
+
+    class Config:
+        validate_all = True
+
+
+validated = PydanticTypes()
+validated.my_pyobject_str(2021, 1, 1)
+validated.my_pyobject_callable(2021, 1, 1)
+validated.my_uuid1.hex
+validated.my_uuid1_str.hex
+validated.my_file_path.absolute()
+validated.my_file_path_str.absolute()
+validated.my_dir_path.absolute()
+validated.my_dir_path_str.absolute()