Skip to content

Conversation

Viicos
Copy link
Member

@Viicos Viicos commented Nov 5, 2024

By adding a new evaluated attribute that can be set during model fields collection. Also refactor a bit the utility functions to eval such annotations.

Change Summary

Related issue number

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review, please add a comment including the phrase "please review" to assign reviewers

@github-actions github-actions bot added the relnotes-fix Used for bugfixes. label Nov 5, 2024
@@ -1215,11 +1215,15 @@ def _common_field_schema( # C901
) -> _CommonField:
# Update FieldInfo annotation if appropriate:
FieldInfo = import_cached_field_info()
if has_instance_in_type(field_info.annotation, (ForwardRef, str)):
Copy link
Member Author

Choose a reason for hiding this comment

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

The interesting part is that we remove the call to has_instance_in_type here.

Copy link

cloudflare-workers-and-pages bot commented Nov 5, 2024

Deploying pydantic-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 076da3c
Status: ✅  Deploy successful!
Preview URL: https://bf7a63be.pydantic-docs.pages.dev
Branch Preview URL: https://fieldinfo-evaluated.pydantic-docs.pages.dev

View logs

@@ -1338,12 +1342,13 @@ def _type_alias_type_schema(self, obj: TypeAliasType) -> CoreSchema:
return maybe_schema

origin: TypeAliasType = get_origin(obj) or obj

annotation = origin.__value__
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 below, as accessing the __value__ attribute will trigger the evaluation of the annotation, and thus this can raise a NameError. The eval_type call is still necessary though, as you can have explicit string references inside a a type alias type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'm misunderstanding - was this moved below? Or do we not need this specific line anymore?

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically, here is what can happen with type aliases:

type A = Int

annotation = origin.__value__
# NameError, 'Int' is not defined

type B = 'Int'
annotation = origin.__value__
assert annotation == 'Int'

eval_type(B, ..., ...)
# NameError, 'Int' is not defined

So everything was moved in a single try..except block:

try:
    annotation = _typing_extra.eval_type(origin.__value__, *self._types_namespace)
except ...: ...

Copy link

codspeed-hq bot commented Nov 5, 2024

CodSpeed Performance Report

Merging #10769 will improve performances by 8.54%

Comparing fieldinfo-evaluated (076da3c) with main (6e91707)

Summary

⚡ 5 improvements
✅ 39 untouched benchmarks

Benchmarks breakdown

Benchmark main fieldinfo-evaluated Change
test_complex_model_schema_generation 2.8 ms 2.6 ms +6.33%
test_lots_of_models_with_lots_of_fields 3.6 s 3.3 s +8.54%
test_simple_model_schema_lots_of_fields_generation 41.8 ms 38.8 ms +7.73%
test_nested_recursive_generic_model_schema_generation 2.3 ms 2.2 ms +5.79%
test_nested_recursive_model_schema_generation 2.3 ms 2.2 ms +6.21%

@Viicos Viicos force-pushed the fieldinfo-evaluated branch 2 times, most recently from 367f48a to 88aa322 Compare November 5, 2024 17:06
Copy link
Contributor

github-actions bot commented Nov 5, 2024

Coverage report

This PR does not seem to contain any modification to coverable code.

Copy link
Contributor

@sydney-runkle sydney-runkle left a comment

Choose a reason for hiding this comment

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

Alrighty, left some initial comments on the implementation.

The tuple return doesn't feel like the cleanest implementation, but I can't come up with anything better off the top of my head. Let's chat first thing tomorrow morning about alternatives. I'm not opposed to this approach, but want to think about other options before we move forward 👍

Comment on lines -283 to +559
'`eval_type_lenient` is deprecated, use `eval_type` with `lenient=True` instead.',
'`eval_type_lenient` is deprecated, use `try_eval_type` instead.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, this feels like an anti-pattern - if we already deprecated this, there's probably a reason why we didn't want to have two functions. Even if we disagree with this deprecation now, seems like a lot of churn to basically reintroduce the function and redirect for the deprecated case...

I believe my suggestion above re always returning a second value, evaluated, even in the case of eval_type with lenient=False might help, and we can leave this be?

Copy link
Contributor

Choose a reason for hiding this comment

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

A few more follow ups:

  1. This is a very internal function - can we just deprecate this?
  2. If we were to bring back a second function, to reduce churn, can we call it eval_type_lenient?

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 had to keep this function during the types namespaces refactor because fastapi makes use of it 🫤. I added the deprecated decorator so that type checking fails in the fastapi CI and they can use the correct function (or best, do not rely on our internals because I don't think the _typing_extra module is stable enough, we might apply other changes here in the future).

I believe my suggestion above re always returning a second value, evaluated, even in the case of eval_type with lenient=False might help, and we can leave this be?

Same issue as https://github.com/pydantic/pydantic/pull/10769/files#r1832947088, we can't have a single function because we want to catch the NameError to reraise PydanticUndefinedAnnotation

@@ -1338,12 +1342,13 @@ def _type_alias_type_schema(self, obj: TypeAliasType) -> CoreSchema:
return maybe_schema

origin: TypeAliasType = get_origin(obj) or obj

annotation = origin.__value__
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'm misunderstanding - was this moved below? Or do we not need this specific line anymore?

@sydney-runkle
Copy link
Contributor

Screenshot 2024-11-07 at 6 23 39 AM

Wow @Viicos, nice work here. Didn't realize this would have that significant of an impact.

We can chat about potential other APIs for this, but we should definitely move forward with this change :).

Does this unblock the _typing_extra.py refactor PR?

@sydney-runkle
Copy link
Contributor

Screenshot 2024-11-07 at 6 25 04 AM

This is great. I presume next we'll look at _apply_annotations, and we know there's a lot to gain there.

@sydney-runkle sydney-runkle added topic-performance and removed relnotes-fix Used for bugfixes. labels Nov 7, 2024
@sydney-runkle
Copy link
Contributor

sydney-runkle commented Nov 7, 2024

Another follow up - what if we returned something like this:

from python import TypedDict

class TypeEvaluationResult(TypedDict):
    evaluated: bool
    result: Any
    error: NameError | None

Or something like:

from python import TypedDict, TypeAlias

class TypeEvaluationSuccess(TypedDict):
    evaluated: Literal[True]
    result: Any

class TypeEvaluationFailure(TypedDict):
    evaluated: Literal[False]
    error: NameError

type TypeEvaluationResult = TypeEvaluationSuccess | TypeEvaluationFailure

Doesn't have to be either of those exactly, but I think you get the point :). Then, we could have one function and preserve relevant errors, but also intuitively handle attempted evals?

Maybe this isn't better, but I think it does help us avoid the overloads, though admittedly there's more conditional logic required after a function call, as the result can be many things.

Could even do a named tuple.

@sydney-runkle sydney-runkle mentioned this pull request Nov 7, 2024
5 tasks
@Viicos
Copy link
Member Author

Viicos commented Nov 8, 2024

Seems like both the overload and structured return value have different drawbacks: what is result if it failed to evaluate? How do you safely make use of result, even if you checked that error is None before ? (as type checkers will complain).

Maybe it's worth revisiting the API, if we want to make this module more stable and maybe expose it publicly, as it is used already by some other projects.

@sydney-runkle
Copy link
Contributor

Yeah, fair point. Let's not go with the structured return.

One other idea - could we revert to that single signature idea, and manufacture a NameError if evaluated is False based on the input?

@Viicos
Copy link
Member Author

Viicos commented Nov 8, 2024

You mean something like dict[str, tuple[Any, bool | NameError]] in the return type?

@sydney-runkle
Copy link
Contributor

Or even like dict[str, tuple[Any, bool], but when the second value is false and we want to raise on failure, just then raise PydanticUndefinedAnnotation(<information about the failed eval similar to what the NameError would have provided>)

@Viicos
Copy link
Member Author

Viicos commented Nov 12, 2024

but the issue is we currently raise using PydanticUndefinedAnnotation.from_name_error so that's why we need the exception to be raised originally.

dict[str, tuple[Any, bool | NameError]] could work but just moves the burden of checking for bool (actually Literal[True]) and NameError to the caller.

Copy link
Contributor

@sydney-runkle sydney-runkle left a comment

Choose a reason for hiding this comment

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

Looking over this again, seems reasonable, given the constraints we've discussed re typing and overloads.

@sydney-runkle
Copy link
Contributor

Going to go ahead and merge. We can cherry pick relevant stuff for the v2.10 official release (not including this).

@sydney-runkle sydney-runkle added relnotes-performance Used for performance improvements. and removed topic-performance labels Nov 14, 2024
@sydney-runkle sydney-runkle enabled auto-merge (squash) November 14, 2024 16:07
By adding a new `evaluated` attribute that can be set during model
fields collection. Also refactor a bit the utility functions to eval
such annotations.
@Viicos Viicos force-pushed the fieldinfo-evaluated branch from ed6e232 to 076da3c Compare November 14, 2024 16:47
@sydney-runkle sydney-runkle merged commit bcfd413 into main Nov 14, 2024
52 checks passed
@sydney-runkle sydney-runkle deleted the fieldinfo-evaluated branch November 14, 2024 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
relnotes-performance Used for performance improvements.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants