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 pathlib brain to handle .parents inference #1442

Merged
merged 43 commits into from
Jun 5, 2022

Conversation

deepyaman
Copy link
Contributor

@deepyaman deepyaman commented Mar 3, 2022

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

The PurePath.parents sequence supports slices since Python 3.10. This brain allows pylint to understand that passing an index (as opposed to a slice) to PurePath.parents will still yield a path, not a tuple.

Given test_index.py:

from pathlib import Path


def _main() -> None:
    current_path = Path().resolve()
    parent_path = current_path.parents[2].resolve()
    print(f"{current_path=}")
    print(f"{parent_path=}")


if __name__ == "__main__":
    _main()

Running pylint yields:

(astroid-3.10) WASH-178551-C02X31K9JHD4:customerone deepyaman_datta$ pylint test_index.py --disable missing-module-docstring

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

Given test_slice.py:

from pathlib import Path


def _main() -> None:
    current_path = Path().resolve()
    parent_paths = [p.resolve() for p in current_path.parents[:2]]
    print(f"{current_path=}")
    print(f"{parent_paths=}")


if __name__ == "__main__":
    _main()

Running pylint yields:

(astroid-3.10) WASH-178551-C02X31K9JHD4:customerone deepyaman_datta$ pylint test_slice.py --disable missing-module-docstring

--------------------------------------------------------------------
Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

Type of Changes

Type
βœ“ πŸ› Bug fix
✨ New feature
πŸ”¨ Refactoring
πŸ“œ Docs

Related Issue

Closes with a test case pylint-dev/pylint#5783
Partially addresses pylint-dev/pylint#5832

@deepyaman deepyaman changed the title Create brain_pathlib.py Add pathlib brain to handle parents in Python 3.10 Mar 4, 2022
@deepyaman deepyaman marked this pull request as ready for review March 4, 2022 02:43
@Pierre-Sassoulas Pierre-Sassoulas added Bug πŸͺ³ Needs review πŸ” Needs to be reviewed by one or multiple more persons python 3.10 labels Mar 4, 2022
@Pierre-Sassoulas Pierre-Sassoulas added this to the 2.11.0 milestone Mar 4, 2022
@DanielNoord DanielNoord self-requested a review March 4, 2022 07:51
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

Thanks @deepyaman, left some initial comments!

I also updated your OP not to directly close the pylint issue. We tend to fix issues in astroid and then add a regression test to pylint to fully close the issue. If you want you can make a PR with a regression test for pylint as well!

astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
@@ -0,0 +1,42 @@
import pytest
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just for "totality" could you also add a test for the inference of parents itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel like I'm almost there, but it's not currently working for:

from pathlib import Path


def _main() -> None:
    current_path = Path().resolve()
    path_parents = current_path.parents
    parent_path = path_parents[2].resolve()
    print(f"{current_path=}")
    print(f"{parent_path=}")


if __name__ == "__main__":
    _main()

It fails with the same test.py:7:18: E1101: Instance of 'tuple' has no 'resolve' member (no-member).

Seems to be why my attempt at adding a nicer-looking test failed:

def test_inference_parents_subscript_index() -> None:
    """Test inference of ``pathlib.Path.parents``, accessed by index."""
    parents, path = astroid.extract_node(
        """
    from pathlib import Path

    current_path = Path().resolve()
    path_parents = current_path.parents
    path_parents  #@
    path_parents[2]  #@
    """
    )
    inferred = parents.inferred()
    assert len(inferred) == 1
    assert isinstance(inferred[0], bases.Instance)
    assert inferred[0].qname() == "pathlib._PathParents"

    inferred = path.inferred()
    assert len(inferred) == 1
    if PY310_PLUS:
        assert isinstance(inferred[0], bases.Instance)
        assert inferred[0].qname() == "pathlib.Path"
    else:
        assert inferred[0] is Uninferable

astroid/brain/brain_pathlib.py Show resolved Hide resolved
@DanielNoord DanielNoord removed the Needs review πŸ” Needs to be reviewed by one or multiple more persons label Mar 4, 2022
@deepyaman
Copy link
Contributor Author

@DanielNoord Thank you for the detailed feedback! I will try and implement the requested changes/feedback over the next few days and get back to you. :)

@cdce8p cdce8p self-requested a review March 4, 2022 21:04
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
tests/unittest_brain_pathlib.py Outdated Show resolved Hide resolved
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
@cdce8p cdce8p modified the milestones: 2.11.0, 2.12.0 Mar 9, 2022
@DanielNoord DanielNoord self-requested a review April 28, 2022 10:01
@DanielNoord
Copy link
Collaborator

@deepyaman Sorry for letting this rest so long. I have been quite busy and haven't had the time to look into this enough. I hope to find some time for it this weekend. Again, sorry for the long delay!

@deepyaman
Copy link
Contributor Author

@deepyaman Sorry for letting this rest so long. I have been quite busy and haven't had the time to look into this enough. I hope to find some time for it this weekend. Again, sorry for the long delay!

No problem, and thanks again for taking the time to guide me through this!

Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

@deepyaman Sorry this took so long to review. When I finally got to it I was in so deep that it seemed easier to just push the fixes I found.

Sorry for taking over your PR with prior notice like that.

In this new iteration we now also correctly infer an index lookup. I do this by creating an inference of the normal ClassDef for Path and then instantiating an instance from it.
I am not completely sure Uninferable for a Slice is correct on <3.9. I'll let another maintainer comment on that: do we infer Uninferable for code that would raise warnings?

I'll re-ask @cdce8p for a review since he had some valuable input in the first round.

@DanielNoord DanielNoord requested review from cdce8p and removed request for cdce8p June 2, 2022 08:36
@DanielNoord DanielNoord added the Needs review πŸ” Needs to be reviewed by one or multiple more persons label Jun 2, 2022
Copy link
Contributor Author

@deepyaman deepyaman left a comment

Choose a reason for hiding this comment

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

@DanielNoord No problem! Thanks for resolving it!

@@ -72,7 +77,7 @@ class A:
parents = 42

c = A()
error = c.parents[2]
error = c.parents[:2]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did this have to be changed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Originally I hadn't fixed the normal index look up, so this was not really testing anything:
The brain returned Uninferable and the non-brain would return Uninferable. So we weren't actually testing if this doesn't actually enter the brain. By using the slice we can see that this doesn't trigger the brain as it would give a different result for PY3.10+.

Now that I fixed the brain to work with index as well the change is perhaps not necessary anymore.

astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
Co-authored-by: Deepyaman Datta <deepyaman.datta@utexas.edu>
ChangeLog Show resolved Hide resolved
Comment on lines 13 to 18
PATH = extract_node(
"""from pathlib import Path
Path
"""
)
PATH_CLASSDEF: nodes.ClassDef = next(PATH.infer())
Copy link
Member

Choose a reason for hiding this comment

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

Inference calls are non-trivial. I'm don't think it's a good idea to do them at import. Better to only define the code template as global const and call extract_node (or even _extract_single_node) in infer_parents_subscript.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But it isn't it better to use the actual ClassDef? You're more experienced here, but this seems to pass tests and doesn't this give us more information than a constructed mimic of Path?

Copy link
Member

Choose a reason for hiding this comment

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

It's probably fine to use extract_node and infer to get the ClassDef, just don't do it at Module level / import time.

As example, this is how the typing brain does something similar
https://github.com/PyCQA/astroid/blob/e57ac42bca80c105bbdad4bae61637b39288a47b/astroid/brain/brain_typing.py#L99-L103
https://github.com/PyCQA/astroid/blob/e57ac42bca80c105bbdad4bae61637b39288a47b/astroid/brain/brain_typing.py#L167-L168

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll refactor!

astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
astroid/brain/brain_pathlib.py Outdated Show resolved Hide resolved
DanielNoord and others added 2 commits June 5, 2022 22:55
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
ChangeLog Outdated Show resolved Hide resolved
Comment on lines 13 to 18
PATH = extract_node(
"""from pathlib import Path
Path
"""
)
PATH_CLASSDEF: nodes.ClassDef = next(PATH.infer())
Copy link
Member

Choose a reason for hiding this comment

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

It's probably fine to use extract_node and infer to get the ClassDef, just don't do it at Module level / import time.

As example, this is how the typing brain does something similar
https://github.com/PyCQA/astroid/blob/e57ac42bca80c105bbdad4bae61637b39288a47b/astroid/brain/brain_typing.py#L99-L103
https://github.com/PyCQA/astroid/blob/e57ac42bca80c105bbdad4bae61637b39288a47b/astroid/brain/brain_typing.py#L167-L168

@cdce8p
Copy link
Member

cdce8p commented Jun 5, 2022

@DanielNoord I moved extract_node inside infer_parents_subscript as well. We should try to do as little computation as possible in the global scope. With iter, I switched the one-item tuple with a list. Although correct, it's quite easy to accidentally remove the trailing comma or overlook it. A list is a bit safer there.

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.

Good to merge once the CI checks pass.

@DanielNoord
Copy link
Collaborator

@DanielNoord I moved extract_node inside infer_parents_subscript as well. We should try to do as little computation as possible in the global scope. With iter, I switched the one-item tuple with a list. Although correct, it's quite easy to accidentally remove the trailing comma or overlook it. A list is a bit safer there.

Thanks! I'm still finding my way through the astroid brains. I'll keep the global computation in mind πŸ˜„

@cdce8p
Copy link
Member

cdce8p commented Jun 5, 2022

I'll keep the global computation in mind πŸ˜„

Off-topic: There is an open proposal for lazy imports in Python: PEP 690. Thankfully opt-in, but nevertheless will be quite challenging as it is to make pylint and astroid compatible.

@DanielNoord DanielNoord merged commit 9b114ad into pylint-dev:main Jun 5, 2022
@DanielNoord
Copy link
Collaborator

Thanks @deepyaman for the initial PR and thanks to @cdce8p for the reviews!

Happy to finally see this merged!

@deepyaman deepyaman deleted the patch-1 branch June 7, 2022 00:45
@jacobtylerwalls jacobtylerwalls removed the Needs review πŸ” Needs to be reviewed by one or multiple more persons label Jun 28, 2022
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

6 participants