Repository: samuelcolvin/pydantic
Pull request #4267 (2022-07-22 15:43:48)

ci: add python 3.11.0b4

samuelcolvin: Looks like this failure should be easy to fix, either by removing or skipping the test or catching the warning.

@PrettyWood will you fix this?
samuelcolvin: :eyes: have we found something real?
PrettyWood: @samuelcolvin 
```py
In [1]: from typing import *

In [2]: get_args(Tuple[()])
Out[2]: ((),)

In [3]: get_args(tuple[()])
Out[3]: ()

In [4]: get_args(tuple)
Out[4]: ()
```
🤦 
PrettyWood: I suspect `typing.Tuple` to actually be `tuple` in 3.11. But now we don't differentiate `tuple[()]` and `tuple`.
I guess we could change `get_args` to return `None` when nothing is defined or add an explicit check to `__args__`, which is defined only for `tuple[()]`...
This whole typing situation starts to be annoying. Every single version adds something new
samuelcolvin: > This whole typing situation starts to be annoying. Every single version adds something new

I agree completely. But then ... that's one of the reason people use pydantic, we wrap that mess in something nice.

On this occasion, it looks like 3.11 is more correct than 3.10:

```py 
>>> from typing import get_args, Tuple
>>> get_args(Tuple[()])
()
>>> get_args(tuple[()])
()
>>> get_args(tuple)
()
>>> sys.version
'3.11.0b4 (main, Jul 22 2022, 17:54:47) [GCC 11.2.0]'
```

PrettyWood: Yeah in 3.11 `Tuple` is `tuple` so this mess is over. But it highlights a bug in 3.10: we don't support `tuple[()]`
samuelcolvin: but what does `tuple[()]` mean as a type annotation? Surely it's invalid?
PrettyWood: Empty tuple
samuelcolvin: Ye true (sorry, long day).

I think that's not the most serious bug in pydantic v1. Any solution that's consistent between python versions and clear to the user is fine for me.
PrettyWood: Ok I think I fixed everything. I didn't do anything complicated and tried to be as explicit as possible in the commits. Please review 🙏 
hramezani: Thanks @PrettyWood for the patch :+1: 

it seems this is [part of 3.11 changes](https://docs.python.org/3.11/whatsnew/3.11.html#typing)
> The representation of empty tuple types (Tuple[()]) is simplified. This affects introspection, e.g. get_args(Tuple[()]) now evaluates to () instead of ((),). (Contributed by Serhiy Storchaka in [gh-91137](https://github.com/python/cpython/issues/91137).)

So, the question is which representation is the right one (`((),)` or `()`)?

BTW, The changes LGTM
samuelcolvin: I think `()` is correct, but pydantic until now returns `((),)`, hence keeping it the same.

LGTM, I'll check again and merge tomorrow.
PrettyWood: @samuelcolvin @hramezani the fix for `Tuple[()]` didn't handle `tuple[()]`. I think the new fix is better. I added some tests for this and a comment. We'll probably clarify things for pydantic v2
PrettyWood: Ok I fixed the test when compiled with `cython`.
I think it's an edge case that is almost never used so it's ok to "sit on it".
Still I reckon it's a good idea to have the test to track this. Since pydantic v2 will be a full python package, it should make things work

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 240a25bce..87144740e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -103,7 +103,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.7', '3.8', '3.9', '3.10', '3.11.0-beta.4']
     env:
       PYTHON: ${{ matrix.python-version }}
       OS: ubuntu
diff --git a/pydantic/schema.py b/pydantic/schema.py
index d37e82206..4f29bd5c7 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -667,13 +667,11 @@ def enum_process_schema(enum: Type[Enum], *, field: Optional[ModelField] = None)
 
     This is similar to the `model_process_schema` function, but applies to ``Enum`` objects.
     """
-    from inspect import getdoc
-
     schema_: Dict[str, Any] = {
         'title': enum.__name__,
         # Python assigns all enums a default docstring value of 'An enumeration', so
         # all enums will have a description field even if not explicitly provided.
-        'description': getdoc(enum),
+        'description': enum.__doc__ or 'An enumeration.',
         # Add enum values and the enum field type to the schema.
         'enum': [item.value for item in cast(Iterable[Enum], enum)],
     }
diff --git a/pydantic/typing.py b/pydantic/typing.py
index 44fba3610..5a70a593a 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -132,6 +132,15 @@ else:
         """
         if hasattr(tp, '_nparams'):
             return (Any,) * tp._nparams
+        # Special case for `tuple[()]`, which used to return ((),) with `typing.Tuple`
+        # in python 3.10- but now returns () for `tuple` and `Tuple`.
+        # This will probably be clarified in pydantic v2
+        try:
+            if tp == Tuple[()] or sys.version_info >= (3, 9) and tp == tuple[()]:  # type: ignore[misc]
+                return ((),)
+        # there is a TypeError when compiled with cython
+        except TypeError:  # pragma: no cover
+            pass
         return ()
 
     def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
diff --git a/pydantic/validators.py b/pydantic/validators.py
index d4783d97b..2c6565802 100644
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -698,7 +698,7 @@ def find_validators(  # noqa: C901 (ignore complexity)
 
     class_ = get_class(type_)
     if class_ is not None:
-        if isinstance(class_, type):
+        if class_ is not Any and isinstance(class_, type):
             yield make_class_validator(class_)
         else:
             yield any_class_validator
diff --git a/setup.cfg b/setup.cfg
index ed77c38ea..a3fb54b9e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,6 +8,9 @@ filterwarnings =
     # for Python 3.10+: mypy still relies on distutils on windows. We hence ignore those warnings
     ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning
     ignore:The distutils.sysconfig module is deprecated, use sysconfig instead:DeprecationWarning
+    # for Python 3.11
+    ignore:path is deprecated.*:DeprecationWarning:certifi
+    ignore:module 'sre_constants' is deprecated:DeprecationWarning:pkg_resources
 
 [flake8]
 max-line-length = 120


Test patch:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index b7083d058..4b6c056c1 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -14,6 +14,7 @@ from pydantic import (
     NoneStrBytes,
     StrBytes,
     ValidationError,
+    compiled,
     constr,
     errors,
     validate_model,
@@ -236,6 +237,97 @@ def test_tuple_more():
     }
 
 
+@pytest.mark.parametrize(
+    'dict_cls,frozenset_cls,list_cls,set_cls,tuple_cls,type_cls',
+    [
+        (Dict, FrozenSet, List, Set, Tuple, Type),
+        (dict, frozenset, list, set, tuple, type),
+    ],
+)
+@pytest.mark.skipif(
+    sys.version_info < (3, 9) or compiled, reason='PEP585 generics only supported for python 3.9 and above'
+)
+def test_pep585_generic_types(dict_cls, frozenset_cls, list_cls, set_cls, tuple_cls, type_cls):
+    class Type1:
+        pass
+
+    class Type2:
+        pass
+
+    class Model(BaseModel, arbitrary_types_allowed=True):
+        a: dict_cls
+        a1: dict_cls[str, int]
+        b: frozenset_cls
+        b1: frozenset_cls[int]
+        c: list_cls
+        c1: list_cls[int]
+        d: set_cls
+        d1: set_cls[int]
+        e: tuple_cls
+        e1: tuple_cls[int]
+        e2: tuple_cls[int, ...]
+        e3: tuple_cls[()]
+        f: type_cls
+        f1: type_cls[Type1]
+
+    default_model_kwargs = dict(
+        a={},
+        a1={'a': '1'},
+        b=[],
+        b1=('1',),
+        c=[],
+        c1=('1',),
+        d=[],
+        d1=['1'],
+        e=[],
+        e1=['1'],
+        e2=['1', '2'],
+        e3=[],
+        f=Type1,
+        f1=Type1,
+    )
+
+    m = Model(**default_model_kwargs)
+    assert m.a == {}
+    assert m.a1 == {'a': 1}
+    assert m.b == frozenset()
+    assert m.b1 == frozenset({1})
+    assert m.c == []
+    assert m.c1 == [1]
+    assert m.d == set()
+    assert m.d1 == {1}
+    assert m.e == ()
+    assert m.e1 == (1,)
+    assert m.e2 == (1, 2)
+    assert m.e3 == ()
+    assert m.f == Type1
+    assert m.f1 == Type1
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(**(default_model_kwargs | {'e3': (1,)}))
+    assert exc_info.value.errors() == [
+        {
+            'ctx': {'actual_length': 1, 'expected_length': 0},
+            'loc': ('e3',),
+            'msg': 'wrong tuple length 1, expected 0',
+            'type': 'value_error.tuple.length',
+        }
+    ]
+
+    Model(**(default_model_kwargs | {'f': Type2}))
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(**(default_model_kwargs | {'f1': Type2}))
+    assert exc_info.value.errors() == [
+        {
+            'ctx': {'expected_class': 'Type1'},
+            'loc': ('f1',),
+            'msg': 'subclass of Type1 expected',
+            'type': 'type_error.subclass',
+        }
+    ]
+
+
 def test_tuple_length_error():
     class Model(BaseModel):
         v: Tuple[int, float, bool]