Repository: samuelcolvin/pydantic
Pull request #3133 (2021-08-25 17:39:29)

Allow ellipsis on Fields inside Annotated
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

Allow ellipsis on Fields inside Annotated. 

The usecase is TypedDicts, which can't have default values otherwise.
Meaning that adding Optionals to Annotated drops them from the list of
required fields, which the patch fixes.

I believe the change is minor enough to not deserve its own issue. 

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


samuelcolvin: please update.
ezegomez: > please update.

should be done (dunno how to remove the "needs author revision" tag)
samuelcolvin: > > please update.
> 
> should be done (dunno how to remove the "needs author revision" tag)

As per the note in the body of this PR, you can just add "please review".

I'm looking now.
samuelcolvin: Good catch, thanks so much.

Patch:
diff --git a/changes/3133-ezegomez.md b/changes/3133-ezegomez.md
new file mode 100644
index 000000000..d813f2ff3
--- /dev/null
+++ b/changes/3133-ezegomez.md
@@ -0,0 +1,1 @@
+Allow ellipsis on `Field`s inside `Annotated` for `TypedDicts` required.
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 09c70b449..ab32be9f2 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -450,7 +450,7 @@ class ModelField(Representation):
             if field_info is not None:
                 field_info = copy.copy(field_info)
                 field_info.update_from_config(field_info_from_config)
-                if field_info.default is not Undefined:
+                if field_info.default not in (Undefined, Required):
                     raise ValueError(f'`Field` default cannot be set in `Annotated` for {field_name!r}')
                 if value is not Undefined and value is not Required:
                     # check also `Required` because of `validate_arguments` that sets `...` as default value


Test patch:
diff --git a/tests/test_annotated_types.py b/tests/test_annotated_types.py
index e9065dfb6..331f99231 100644
--- a/tests/test_annotated_types.py
+++ b/tests/test_annotated_types.py
@@ -6,12 +6,12 @@ Tests for annotated types that _pydantic_ can validate like
 import json
 import sys
 from collections import namedtuple
-from typing import List, NamedTuple, Tuple
+from typing import List, NamedTuple, Optional, Tuple
 
 import pytest
-from typing_extensions import TypedDict
+from typing_extensions import Annotated, TypedDict
 
-from pydantic import BaseModel, PositiveInt, ValidationError
+from pydantic import BaseModel, Field, PositiveInt, ValidationError
 
 if sys.version_info < (3, 9, 2):
     try:
@@ -290,3 +290,42 @@ def test_typeddict_postponed_annotation():
 
     with pytest.raises(ValidationError):
         Model.parse_obj({'t': {'v': -1}})
+
+
+def test_typeddict_annotated_nonoptional():
+    class DataTD(TypedDict):
+        a: Optional[int]
+        b: Annotated[Optional[int], Field(...)]
+        c: Annotated[Optional[int], Field(..., description='Test')]
+        d: Annotated[Optional[int], Field()]
+
+    class Model(BaseModel):
+        data_td: DataTD
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {'data_td': {'$ref': '#/definitions/DataTD'}},
+        'required': ['data_td'],
+        'definitions': {
+            'DataTD': {
+                'type': 'object',
+                'title': 'DataTD',
+                'properties': {
+                    'a': {'title': 'A', 'type': 'integer'},
+                    'b': {'title': 'B', 'type': 'integer'},
+                    'c': {'title': 'C', 'type': 'integer', 'description': 'Test'},
+                    'd': {'title': 'D', 'type': 'integer'},
+                },
+                'required': ['a', 'b', 'c'],
+            },
+        },
+    }
+
+    for bad_obj in ({}, {'data_td': []}, {'data_td': {'a': 1, 'b': 2, 'd': 4}}):
+        with pytest.raises(ValidationError):
+            Model.parse_obj(bad_obj)
+
+    valid_data = {'a': 1, 'b': 2, 'c': 3}
+    parsed_model = Model.parse_obj({'data_td': valid_data})
+    assert parsed_model and parsed_model == Model(data_td=valid_data)