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

Improve inference for generic classes (PEP 695) #2433

Merged
merged 5 commits into from May 19, 2024

Conversation

cdce8p
Copy link
Member

@cdce8p cdce8p commented May 12, 2024

Type of Changes

Type
βœ“ πŸ› Bug fix

Description

Generic classes with PEP 695 should be considered subscriptable, i.e. have a __class_getitem__ method.

class Foo[T]: ...

Refs pylint-dev/pylint#9406
Will close the issue after adding some more tests in pylint.

Copy link

codecov bot commented May 12, 2024

Codecov Report

All modified and coverable lines are covered by tests βœ…

Project coverage is 92.79%. Comparing base (d7a2761) to head (7f71bde).
Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2433      +/-   ##
==========================================
- Coverage   92.79%   92.79%   -0.01%     
==========================================
  Files          94       94              
  Lines       11109    11118       +9     
==========================================
+ Hits        10309    10317       +8     
- Misses        800      801       +1     
Flag Coverage Ξ”
linux 92.59% <100.00%> (-0.01%) ⬇️
pypy 92.79% <100.00%> (-0.01%) ⬇️
windows 92.69% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Ξ”
astroid/brain/brain_typing.py 87.73% <100.00%> (+0.55%) ⬆️
astroid/nodes/scoped_nodes/scoped_nodes.py 92.52% <ΓΈ> (ΓΈ)
astroid/protocols.py 90.28% <100.00%> (ΓΈ)

... and 2 files with indirect coverage changes

@cdce8p cdce8p marked this pull request as draft May 12, 2024 22:46
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

LGTM

astroid/brain/brain_typing.py Outdated Show resolved Hide resolved
@jacobtylerwalls jacobtylerwalls self-requested a review May 13, 2024 13:04
) -> Iterator[ClassDef]:
"""Add __class_getitem__ for generic classes. Python 3.12+."""
try:
value = next(node.infer())
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for tackling this. One question for my own information: why do we need to use inference? Can we just check node.type_params?

Copy link
Member Author

Choose a reason for hiding this comment

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

Tbh I'm not quite sure either. All other methods do this as well. My best guess is that with the brain_** we overwrite the default inference. However, since we still want that, we need to call node.infer() ourselves.

Copy link
Member

Choose a reason for hiding this comment

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

Interestingly, if I add assert node is value, it passes.

Can we use this opportunity to explore avoiding inference here?

@cdce8p cdce8p changed the title Fix unsubscriptable-object for generic classes (PEP 695) Improve inference for generic classes (PEP 695) May 16, 2024
@cdce8p cdce8p marked this pull request as ready for review May 16, 2024 22:02
Comment on lines +927 to +930
"""Hack. Return any Node so inference doesn't fail
when evaluating __class_getitem__. Revert if it's causing issues.
"""
return
yield
yield nodes.Const(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 really isn't the "correct" solution but it works as the result is ignored anyway when inferring the __class_getitem__ call. It just can't raise an Exception - that would lead to Uninferable.

The typing brain solves that by inferring the T = TypeVar(...) calls as new classes which is better but probably also not fully right.

If it was actually uses, I believe we would've seen issue reports for it by now.

typename = node.args[0].as_string().strip("'")
try:
node = extract_node(TYPING_TYPE_TEMPLATE.format(typename))
except AstroidSyntaxError as exc:
raise InferenceError from exc
return node.infer(context=context_itton)

TYPING_TYPE_TEMPLATE = """
class Meta(type):
def __getitem__(self, item):
return self
@property
def __args__(self):
return ()
class {0}(metaclass=Meta):
pass
"""

@cdce8p cdce8p added this to the 3.2.2 milestone May 16, 2024
Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

Open to different ideas here, but I think this would be a nice opportunity to verify things still work if we don't use inference.

@jacobtylerwalls
Copy link
Member

Should we guard this under if node.type_params: ?

@cdce8p
Copy link
Member Author

cdce8p commented May 19, 2024

Should we guard this under if node.type_params: ?

That is done by the predicate. No need to duplicate it I think

def _looks_like_generic_class_pep695(node: ClassDef) -> bool:
    """Check if class is using type parameter. Python 3.12+."""
    return len(node.type_params) > 0

@cdce8p
Copy link
Member Author

cdce8p commented May 19, 2024

Can we use this opportunity to explore avoiding inference here?

Investigated that a bit more. With the previous setup, we would end up calling the inference_tip twice as the infer call was on node itself. All other function is brain_typing infer an attribute like func or args[0]. As the second call happened during the first inference pass, it would just get filtered out by the inference cache. So it's fine to remove it here. Also tested it with Home Assistant were everything continues to work as expected.

if partial_cache_key in _CURRENTLY_INFERRING:
# If through recursion we end up trying to infer the same
# func + node we raise here.
_CURRENTLY_INFERRING.remove(partial_cache_key)
raise UseInferenceDefault

@cdce8p cdce8p merged commit fadac92 into pylint-dev:main May 19, 2024
19 of 20 checks passed
@cdce8p cdce8p deleted the fix-pep695 branch May 19, 2024 21:45
github-actions bot pushed a commit that referenced this pull request May 19, 2024
cdce8p added a commit that referenced this pull request May 19, 2024
(cherry picked from commit fadac92)

Co-authored-by: Marc Mueller <[email protected]>
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.

None yet

3 participants