Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not try to automatically handle subclasses of mappings #5744

Merged
merged 5 commits into from
May 15, 2023

Conversation

adriangb
Copy link
Member

@adriangb adriangb commented May 11, 2023

  • Better handling of constraints for mappings
  • Better handling of DefaultDict
    • only infer a default factory for obvious cases (int, float, bool, list, set and dict basically) and fail for other cases
    • allow specifying a default factory via OrderedDict[int, Annotated[list, Field(default_factory=...)]]
  • Better handling of OrderedDict

Fixes #3417, #4687

Selected Reviewer: @dmontagu

@cloudflare-pages
Copy link

cloudflare-pages bot commented May 11, 2023

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: 118055e
Status: ✅  Deploy successful!
Preview URL: https://28b690e6.pydantic-docs2.pages.dev
Branch Preview URL: https://mapping-subclasses.pydantic-docs2.pages.dev

View logs

@adriangb adriangb marked this pull request as ready for review May 11, 2023 18:43
@adriangb
Copy link
Member Author

please review

@lig
Copy link
Contributor

lig commented May 11, 2023

I would try copying here this test https://github.com/pydantic/pydantic/pull/5598/files#diff-cbcfeba88238560f75e880d530f4cd82ab6bf90247012e8f664281941f8da1f7R3782 from #5598 as well.

The test were failing before.

@adriangb
Copy link
Member Author

I would try copying here this test https://github.com/pydantic/pydantic/pull/5598/files#diff-cbcfeba88238560f75e880d530f4cd82ab6bf90247012e8f664281941f8da1f7R3782 from #5598 as well.

The test were failing before.

👍🏻 b0655e0

Comment on lines -1654 to -1691
def test_dict_subclasses_bare():
class Model(BaseModel):
a: MyDict

assert repr(Model(a=MyDict({'a': 1})).a) == "MyDict({'a': 1})"
assert repr(Model(a=MyDict({b'x': (1, 2)})).a) == "MyDict({b'x': (1, 2)})"


def test_dict_subclasses_typed():
class Model(BaseModel):
a: MyDict[str, int]

assert repr(Model(a=MyDict({'a': 1})).a) == "MyDict({'a': 1})"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer supported, I added tests for it the error

Comment on lines -1669 to -1729
def test_typing_coercion_defaultdict():
class Model(BaseModel):
x: DefaultDict[int, str]

d = defaultdict(str)
d['1']
m = Model(x=d)
assert isinstance(m.x, defaultdict)
assert repr(m.x) == "defaultdict(<class 'str'>, {1: ''})"


def test_typing_coercion_counter():
class Model(BaseModel):
x: Counter[str]

m = Model(x={'a': 10})
assert isinstance(m.x, Counter)
assert repr(m.x) == "Counter({'a': 10})"


def test_typing_counter_value_validation():
class Model(BaseModel):
x: Counter[str]

with pytest.raises(ValidationError) as exc_info:
Model(x={'a': 'a'})

# insert_assert(exc_info.value.errors(include_url=False))
assert exc_info.value.errors(include_url=False) == [
{
'type': 'int_parsing',
'loc': ('x', 'a'),
'msg': 'Input should be a valid integer, unable to parse string as an integer',
'input': 'a',
}
]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to test_types.py

Comment on lines -1605 to -1641
def test_mapping_retains_type_defaultdict():
class Model(BaseModel):
x: Mapping[str, int]

d = defaultdict(int)
d['foo'] = '2'
d['bar']

m = Model(x=d)
assert isinstance(m.x, defaultdict)
assert m.x['foo'] == 2
assert m.x['bar'] == 0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer retain the input type (although we do retain the default value). This also duplicates some of test_replace_types_with_user_defined_generic_type_field

Copy link
Contributor

@dmontagu dmontagu May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea to document this in the migration notes, I'll add it to the list (edit: done: https://github.com/orgs/pydantic/projects/3/views/1)

# `OrderedDict[Any, Any]`
return _ordered_dict_any_schema()
# Assume Annotated[..., Field(...)]
field_info = next((v for v in get_args(values_source_type) if isinstance(v, FieldInfo)), None)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for now, but I feel like it’s a bit hacky. I think we should try to better integrate this later, eg to give a meaningful error if Field(default_factory=…) is used in places where it isn’t allowed (we currently ignore it)

Copy link
Contributor

@dmontagu dmontagu May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worries me a bit to not actually check that values_source_type is actually Annotated before iterating over the get_args call's results. I'm thinking we could do something like this:

if hasattr(obj, '__origin__') and not isinstance(obj, type(Annotated[int, 123])):

to ensure this and make it safer here in the event that get_args starts handling more things in the future or whatever.

(Might make sense to make that a utility function or similar anyway.)

If you disagree, I don't feel strongly.

@@ -473,26 +464,10 @@ def _generate_schema(self, obj: Any) -> core_schema.CoreSchema: # noqa: C901
elif issubclass(origin, typing.Tuple): # type: ignore[arg-type]
# TODO: To support generic subclasses of typing.Tuple, we need to better-introspect the args to origin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment be removed? Not sure how you feel about generic subclasses of Tuple in the context of this PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do this work for Tuple as well, but I'm not going to do it in this PR, it's already huge 😆

Copy link
Contributor

@dmontagu dmontagu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this will cause custom types from v1 to break in v2, we should probably include a warning note in the migration guide and/or next release's release notes.

Copy link
Contributor

@dmontagu dmontagu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lig lig mentioned this pull request May 15, 2023
5 tasks
@adriangb adriangb enabled auto-merge (squash) May 15, 2023 13:07
@adriangb adriangb disabled auto-merge May 15, 2023 13:07
@adriangb adriangb enabled auto-merge (squash) May 15, 2023 13:08
@adriangb adriangb merged commit 70e7e99 into main May 15, 2023
39 checks passed
@adriangb adriangb deleted the mapping-subclasses branch May 15, 2023 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

DefaultDict default function gets replaced with type creator in object creation
3 participants