8000 Fix `NameError` when using `validate_call` with PEP 695 on a class by kc0506 · Pull Request #10380 · pydantic/pydantic · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@kc0506
Copy link
Contributor
@kc0506 kc0506 commented Sep 11, 2024

Change Summary

This PR fixes the NameError when from __future__ import annotations and we call validate_call inside a class definition with PEP 695:

from __future__ import annotations

class A[T]:
    @validate_call(validate_return=True)
    def f(self, a: T) -> T:
        return str(a)

However, calling validate_call in nested scopes will still throw NameError. This is probably due to parent_frame_namespace. Example:

from __future__ import annotations
from pydantic import validate_call

class A[T]:
    def g(self):
        @validate_call(validate_return=True)
        def inner(a: T) -> T: ...
A().g() # throws because we can only get the ns of `g`, not `A` when calling `@validate_call`

Related issue number

fix #10150

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

8000
@github-actions github-actions bot added the relnotes-fix Used for bugfixes. label Sep 11, 2024
@codspeed-hq
Copy link
codspeed-hq bot commented Sep 11, 2024

CodSpeed Performance Report

Merging #10380 will not alter performance

Comparing kc0506:fix-10150 (b42288d) with main (07beaff)

Summary

✅ 49 untouched benchmarks

@github-actions
Copy link
Contributor
github-actions bot commented Sep 11, 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.

LGTM overall, just a few change requests :).

@pydantic-hooky pydantic-hooky bot added awaiting author revision awaiting changes from the PR author labels Sep 12, 2024
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.

Nice work, thanks!!

@sydney-runkle sydney-runkle enabled auto-merge (squash) September 12, 2024 15:44
@sydney-runkle sydney-runkle merged commit 899ff2c into pydantic:main Sep 12, 2024
@kc0506
Copy link
Contributor Author
kc0506 commented Sep 13, 2024

@sydney-runkle I think the nested scope issues here are different from the case of TypeAdapter .
Take this code for example:

from __future__ import annotations

class A[T: int](BaseModel):
    def g1(self):
        return TypeAdapter(list[T])

    def g2(self):
        @validate_call
        def f() -> T: ...

When calling TypeAdapter, the Python bytecode actually use T, so T will be recognized inside the closure when compiling g1's code object. Thus T will be copied into locals at runtime when g1 is called.

On the other hand, the T inside g2 is just pure str, not a reference to T, so from the viewpoint of compiler, T is not even used by g2. Therefore at runtime, the local namespace of g2 will not contain T, which means there is no way to access T inside @validate_call.

So currently I guess the best we could do is to make sure things work without from __future__ import annotations, and perhaps note the issue somewhere of the docs. I hope this make sense for you.

@sydney-runkle
Copy link
Contributor

Gotcha.

My suggestion was that we could add a _parent_depth argument to validate_call to control how deep we look in the parent_frame_namespace, which might resolve the original issue you mentioned. Maybe not though, I haven't tried locally.

@kc0506
Copy link
Contributor Author
kc0506 commented Sep 13, 2024
import inspect
import sys

from pydantic._internal import _typing_extra

# analogy to `validate_call`, we want to access `a` in this function
def api():
    # ! `f` is NOT in stack
    # print([f.function for f in inspect.stack()])  # ['api', 'g', '<module>']

    # print(sys._getframe(1).f_code.co_name)  # 'g'
    # print(sys._getframe(2).f_code.co_name)  # '<module>'

    # parent_depth should add 1 because `parent_frame_namespace` is pushed to call stack
    print('ns of g:', _typing_extra.parent_frame_namespace(parent_depth=2))


def f():
    a = 1

    def g1():
        api()

    def g2():
        a
        api()

    return g1, g2

g1, g2 = f()
g1()  # ns of g: {}
g2()  # ns of g: {'a': 1}

Here is a simple demo. My point is that by the time api is called, f is not in the stack anymore. So we can not find a in the namespace no matter how deep we look in parent frames, unless it is caught by closure, as the case of g2. This behavior should be the same for class definition.

@kc0506 kc0506 deleted the fix-10150 branch October 14, 2024 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author revision awaiting changes from the PR author relnotes-fix Used for bugfixes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"NameError: name 'T' is not defined" when using "from __future__ import annotations" with generic function

2 participants

0