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

Add typing to NodeNG.nodes_of_class #1168

Merged
merged 6 commits into from
Sep 13, 2021

Conversation

DanielNoord
Copy link
Collaborator

@DanielNoord DanielNoord commented Sep 11, 2021

Steps

  • For new features or bug fixes, add a ChangeLog entry describing what your PR does.
  • Write a good description on what the PR does.

Description

This uses overloads to type the nodes_of_class function.

For some reason pylint doesn't pick up on the iterable context correctly. Some help would be appreciated! This is not shown in the CI but does show up locally.

Type of Changes

Type
🔨 Refactoring

Related Issue

This uses overloads to type the ``nodes_of_class`` function
@DanielNoord DanielNoord marked this pull request as draft September 11, 2021 14:09
@DanielNoord DanielNoord marked this pull request as ready for review September 12, 2021 08:32
@DanielNoord
Copy link
Collaborator Author

DanielNoord commented Sep 12, 2021

As said in the description: would like some help figuring out why pylint throws an error. I made a basic test case with overload and pylint did seem to understand them. Not sure why it doesn't pick up that the function will always return an iterable.

EDIT: I have done some additional investigations. The problem is that self.get_children() does not seem to recognise the full function. See the following output:

Pdb) self
<FunctionDef.nodes_of_class l.504 at 0x109915e80>
(Pdb) [i for i in self.get_children()]
[<Decorators l.503 at 0x1094b9580>, <Arguments l.505 at 0x1094b97c0>, <Subscript l.508 at 0x1094b9640>, <Expr l.509 at 0x1094b9220>]

L509 is the ... used in the overload but the other overloads and actual function don't get parsed. This requires some extensive knowledge of the AstroidBuilder, which I don't have. Perhaps one of the maintainers knows if 1) this is a known problem or 2) if there is a possible fix for this or 3) can point me to the function that should be fixed to make this work.

EDIT2: Looks like #1015 has already indicated this

@cdce8p cdce8p self-requested a review September 12, 2021 14:25
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

Thanks for starting the work on this one! It was long overdue.
I left a few comments. Basically, all current overloads are redundant and can be removed.

--
One area we still need to take a look at is the annotation for klass. As this can be a tuple as well, it complicates things. An idea:

T_Nodes = TypeVar("T_Nodes", bound="NodeNG")
T_Nodes2 = TypeVar("T_Nodes2", bound="NodeNG")
T_Nodes3 = TypeVar("T_Nodes3", bound="NodeNG")
SkipKlassT = Union[None, Type["NodeNG"], Tuple[Type["NodeNG"], ...]]


class NodeNG:
    @overload
    def nodes_of_class(
        self,
        klass: Type[T_Nodes],
        skip_klass: SkipKlassT = None,
    ) -> Iterator[T_Nodes]:
        ...

    @overload
    def nodes_of_class(
        self,
        klass: Tuple[Type[T_Nodes], Type[T_Nodes2]],
        skip_klass: SkipKlassT = None,
    ) -> Union[Iterator[T_Nodes], Iterator[T_Nodes2]]:
        ...

    @overload
    def nodes_of_class(
        self,
        klass: Tuple[Type[T_Nodes], Type[T_Nodes2], Type[T_Nodes3]],
        skip_klass: SkipKlassT = None,
    ) -> Union[Iterator[T_Nodes], Iterator[T_Nodes2], Iterator[T_Nodes3]]:
        ...

    # Technically any number of additional overloads with a different number of
    # items for 'klass' could be added. However, 3 should cover most cases
    # Use the last overload as fallback.
    # Unfortunately, this doesn't work in 'mypy'. Probably because of a bug.

    @overload
    def nodes_of_class(
        self,
        klass: Tuple[Type[T_Nodes], ...],
        skip_klass: SkipKlassT = None,
    ) -> Iterator[T_Nodes]:
        ...

    def nodes_of_class(
        self,
        klass: Union[
            Type[T_Nodes],
            Tuple[Type[T_Nodes], Type[T_Nodes2]],
            Tuple[Type[T_Nodes], Type[T_Nodes2], Type[T_Nodes3]],
            Tuple[Type[T_Nodes], ...],  # related to the 'mypy' bug. Needs to be excluded as well
        ],
        skip_klass: SkipKlassT = None,
    ) -> Union[Iterator[T_Nodes], Iterator[T_Nodes2], Iterator[T_Nodes3]]:
        # implementation
        pass

--
Regarding the pylint issue. I would think this is a bug. Need to take a look at it sometime. For now, I would recommend to use # pylint: ignore and link the issue.

astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
@cdce8p cdce8p added the Maintenance Discussion or action around maintaining astroid or the dev workflow label Sep 12, 2021
@cdce8p cdce8p added this to the 2.8.0 milestone Sep 12, 2021
@DanielNoord
Copy link
Collaborator Author

Wow, this is a much cleaner solution! I did not know bound worked like this. Hopefully it didn't take you you too much time to come up with this.
I do think this will be really helpful in tackling some of the mypy issues for pylint as we get much more precise typing now :)
Glad to have put this on your radar!

for yield_ in self.nodes_of_class(node_classes.Yield):
for (
yield_
) in self.nodes_of_class( # pylint: disable=not-an-iterable # See https://github.com/PyCQA/astroid/issues/1015
Copy link
Member

Choose a reason for hiding this comment

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

Just realized that we would need to add this to every call in pylint as well 😕
The issue should probably be fixed before we can merge it.

Copy link
Collaborator Author

@DanielNoord DanielNoord Sep 12, 2021

Choose a reason for hiding this comment

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

I did a pip install -e local_astroid_folder and succeeded in running the pre-commit to test for this. (in my local pylint folder of course)

The problem comes from overloads where the ... is on a separate line (I think).
We don't have those in pylint (yet), so I think for the time-being we are fine.

You might want to check this though by doing pip -e as well, just to be sure!

Copy link
Member

Choose a reason for hiding this comment

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

Just tested it as well and you're right. At the moment pylint doesn't complain. The reason however isn't that we don't use overloads. It's actually that node in visit_xxx(self, node) cannot be inferred and thus no error is emitted. In contrast, just doing something like this will easily produce one:

def func():
    node = nodes.For()
    for n in node.nodes_of_class(nodes.Name):  # not-an-iterable
        pass

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Didn't we just add typing to all visit_xxx(self, node) calls in pylint? Or is that not enough to help mypy?

I saw you just assigned somebody to #1015. I think we should block this until that issue has been fixed, right?

Copy link
Member

Choose a reason for hiding this comment

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

Except from some corner cases, type annotations aren't used to infer values in pylint. That is different in mypy.

I think we should block this until that issue has been fixed, right?

Since this doesn't actually cause any issues with pylint, I would say we can merge it now. It doesn't necessarily need to wait for #1015.

@cdce8p
Copy link
Member

cdce8p commented Sep 12, 2021

Hopefully it didn't take you you too much time to come up with this.

I spend most of the time dealing with type checker issues to be honest. The one I mentioned for mypy and also opened 2 for pyright.

do think this will be really helpful in tackling some of the mypy issues for pylint as we get much more precise typing now :)

We should be able to remove a few typing.cast as well.

@cdce8p
Copy link
Member

cdce8p commented Sep 13, 2021

@DanielNoord Would you like to work on a follow up PR for pylint to remove now redundant typing.cast calls? Searching for = cast(nodes. should probably do the trick.

astroid/nodes/node_ng.py Outdated Show resolved Hide resolved
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

Looks good 🚀
Thanks @DanielNoord!

@DanielNoord
Copy link
Collaborator Author

@DanielNoord Would you like to work on a follow up PR for pylint to remove now redundant typing.cast calls? Searching for = cast(nodes. should probably do the trick.

Yes! You can assign that (imaginary) issue to me 😄

@cdce8p cdce8p merged commit 8fbef58 into pylint-dev:main Sep 13, 2021
@DanielNoord DanielNoord deleted the typing-nodes-of-class branch September 13, 2021 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Maintenance Discussion or action around maintaining astroid or the dev workflow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants